Hello Internals,
I was pondering a little about effect handlers today, and how they could
work as a replacement for dependency injection and mocking. Let me show an
example:
<?php
require_once("vendor/autoload.php");
use Latitude\QueryBuilder\Engine\MySqlEngine;
use Latitude\QueryBuilder\QueryFactory;
use function Latitude\QueryBuilder\field;
// Dummy db connection
class Db
{
public function getQueryBuilder()
{
return new QueryFactory(new MySqlEngine());
}
}
interface Effect {}
class QueryEffect implements Effect
{
public $query;
public function __construct($query)
{
$this->query = $query;
}
}
class Plugin
{
/* The "normal" way to do testing, by injecting the db object. Not
needed here.
public function __construct(Db $db)
{
$this->db = $db;
}
*/
public function populateCreditCardData(&$receipt)
{
foreach ($receipt['items'] as &$item) {
// 2 = credit card
if ($item['payment_type'] == 2) {
$query = $this->db->getQueryBuilder()
->select('card_product_name ')
->from('card_transactions')
->where(field('id')->eq($item['card_transaction_id']))
->compile();
// Normal way: Call the injected dependency class directly.
//$result = $this->db->search($query->sql(),
$query->params());
// Generator way, push the side-effect up the stacktrace
using generators.
$result = yield new QueryEffect($query);
if ($result) {
$item['card_product_name'] =
$result[0]['card_product_name'];
}
}
}
}
}
// Dummy receipt
$receipt = [
'items' => [
[
'payment_type' => 2
]
]
];
$p = new Plugin(); // Database is not injected
$gen = $p->populateCreditCardData($receipt);
foreach ($gen as $effect) {
// Call $db here instead of injecting it.
// But now I have to propagate the $gen logic all over the call stack,
with "yield from"? :(
// Effect handlers solve this by forcing an effect up in the stack
trace similar to exceptions.
// Dummy db result
$rows = [
[
'card_product_name' => 'KLARNA',
]
];
$gen->send($rows);
}
// Receipt item now has card_product_name populated properly.
print_r($receipt);
OK, so the problem with above code is that, in order for it to work, you
have to add "yield from" from the top to the bottom of the call stack,
polluting the code-base similar to what happens with "async" in JavaScript.
Also see the "Which color is your function" article [1].
For this design pattern to work seamlessly, there need to be a way to yield
"all the way", so to speak, similar to what an exception does, and how
effect handlers work in OCaml [2].
The question is, would this be easy, hard, or very hard to add to the
current PHP source code? Is it conceptually too different from generators?
Would it be easier to add a way to "jump back" from a catched exception
(kinda abusing the exception use-case, but that's how effect handlers work,
more or less)?
Thanks for reading :)
Olle
[1] -
https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/
Hello Internals,
I was pondering a little about effect handlers today, and how they could
work as a replacement for dependency injection and mocking. Let me show an
example:<?php
require_once("vendor/autoload.php");
use Latitude\QueryBuilder\Engine\MySqlEngine;
use Latitude\QueryBuilder\QueryFactory;
use function Latitude\QueryBuilder\field;// Dummy db connection
class Db
{
public function getQueryBuilder()
{
return new QueryFactory(new MySqlEngine());
}
}interface Effect {}
class QueryEffect implements Effect
{
public $query;public function __construct($query) { $this->query = $query; }
}
class Plugin
{
/* The "normal" way to do testing, by injecting the db object. Not
needed here.
public function __construct(Db $db)
{
$this->db = $db;
}
*/public function populateCreditCardData(&$receipt) { foreach ($receipt['items'] as &$item) { // 2 = credit card if ($item['payment_type'] == 2) { $query = $this->db->getQueryBuilder() ->select('card_product_name ') ->from('card_transactions') ->where(field('id')->eq($item['card_transaction_id'])) ->compile(); // Normal way: Call the injected dependency class directly. //$result = $this->db->search($query->sql(),
$query->params());
// Generator way, push the side-effect up the stacktrace
using generators.
$result = yield new QueryEffect($query);
if ($result) {
$item['card_product_name'] =
$result[0]['card_product_name'];
}
}
}
}
}// Dummy receipt
$receipt = [
'items' => [
[
'payment_type' => 2
]
]
];
$p = new Plugin(); // Database is not injected
$gen = $p->populateCreditCardData($receipt);
foreach ($gen as $effect) {
// Call $db here instead of injecting it.
// But now I have to propagate the $gen logic all over the call stack,
with "yield from"? :(
// Effect handlers solve this by forcing an effect up in the stack
trace similar to exceptions.// Dummy db result $rows = [ [ 'card_product_name' => 'KLARNA', ] ]; $gen->send($rows);
}
// Receipt item now has card_product_name populated properly.
print_r($receipt);
OK, so the problem with above code is that, in order for it to work, you
have to add "yield from" from the top to the bottom of the call stack,
polluting the code-base similar to what happens with "async" in JavaScript.
Also see the "Which color is your function" article [1].For this design pattern to work seamlessly, there need to be a way to yield
"all the way", so to speak, similar to what an exception does, and how
effect handlers work in OCaml [2].The question is, would this be easy, hard, or very hard to add to the
current PHP source code? Is it conceptually too different from generators?
Would it be easier to add a way to "jump back" from a catched exception
(kinda abusing the exception use-case, but that's how effect handlers work,
more or less)?Thanks for reading :)
Olle
[1] -
https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/
You want to jump out of the current stack frame to one a higher one, and possibly resume execution?
The best way to do that is to use fibers. They basically do exactly this behavior.
— Rob
Hi,
Great tip, thank you, I had forgot about those! Will try.
Olle
Den mån 16 juni 2025 kl 17:32 skrev Rob Landers rob@bottled.codes:
Hello Internals,
I was pondering a little about effect handlers today, and how they could
work as a replacement for dependency injection and mocking. Let me show an
example:<?php
require_once("vendor/autoload.php");
use Latitude\QueryBuilder\Engine\MySqlEngine;
use Latitude\QueryBuilder\QueryFactory;
use function Latitude\QueryBuilder\field;// Dummy db connection
class Db
{
public function getQueryBuilder()
{
return new QueryFactory(new MySqlEngine());
}
}interface Effect {}
class QueryEffect implements Effect
{
public $query;public function __construct($query) { $this->query = $query; }
}
class Plugin
{
/* The "normal" way to do testing, by injecting the db object. Not
needed here.
public function __construct(Db $db)
{
$this->db = $db;
}
*/public function populateCreditCardData(&$receipt) { foreach ($receipt['items'] as &$item) { // 2 = credit card if ($item['payment_type'] == 2) { $query = $this->db->getQueryBuilder() ->select('card_product_name ') ->from('card_transactions') ->where(field('id')->eq($item['card_transaction_id'])) ->compile(); // Normal way: Call the injected dependency class directly. //$result = $this->db->search($query->sql(),
$query->params());
// Generator way, push the side-effect up the stacktrace
using generators.
$result = yield new QueryEffect($query);
if ($result) {
$item['card_product_name'] =
$result[0]['card_product_name'];
}
}
}
}
}// Dummy receipt
$receipt = [
'items' => [
[
'payment_type' => 2
]
]
];
$p = new Plugin(); // Database is not injected
$gen = $p->populateCreditCardData($receipt);
foreach ($gen as $effect) {
// Call $db here instead of injecting it.
// But now I have to propagate the $gen logic all over the call stack,
with "yield from"? :(
// Effect handlers solve this by forcing an effect up in the stack
trace similar to exceptions.// Dummy db result $rows = [ [ 'card_product_name' => 'KLARNA', ] ]; $gen->send($rows);
}
// Receipt item now has card_product_name populated properly.
print_r($receipt);
OK, so the problem with above code is that, in order for it to work, you
have to add "yield from" from the top to the bottom of the call stack,
polluting the code-base similar to what happens with "async" in JavaScript.
Also see the "Which color is your function" article [1].For this design pattern to work seamlessly, there need to be a way to yield
"all the way", so to speak, similar to what an exception does, and how
effect handlers work in OCaml [2].The question is, would this be easy, hard, or very hard to add to the
current PHP source code? Is it conceptually too different from generators?
Would it be easier to add a way to "jump back" from a catched exception
(kinda abusing the exception use-case, but that's how effect handlers work,
more or less)?Thanks for reading :)
Olle
[1] -
https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/[2] - https://ocaml.org/manual/5.3/effects.html
You want to jump out of the current stack frame to one a higher one, and
possibly resume execution?The best way to do that is to use fibers. They basically do exactly this
behavior.— Rob
Hello Internals,
I was pondering a little about effect handlers today, and how they could
work as a replacement for dependency injection and mocking. Let me show an
example:<?php
require_once("vendor/autoload.php");
use Latitude\QueryBuilder\Engine\MySqlEngine;
use Latitude\QueryBuilder\QueryFactory;
use function Latitude\QueryBuilder\field;// Dummy db connection
class Db
{
public function getQueryBuilder()
{
return new QueryFactory(new MySqlEngine());
}
}interface Effect {}
class QueryEffect implements Effect
{
public $query;public function __construct($query) { $this->query = $query; }
}
class Plugin
{
/* The "normal" way to do testing, by injecting the db object. Not
needed here.
public function __construct(Db $db)
{
$this->db = $db;
}
*/public function populateCreditCardData(&$receipt) { foreach ($receipt['items'] as &$item) { // 2 = credit card if ($item['payment_type'] == 2) { $query = $this->db->getQueryBuilder() ->select('card_product_name ') ->from('card_transactions') ->where(field('id')->eq($item['card_transaction_id'])) ->compile(); // Normal way: Call the injected dependency class directly. //$result = $this->db->search($query->sql(),
$query->params());
// Generator way, push the side-effect up the stacktrace
using generators.
$result = yield new QueryEffect($query);
if ($result) {
$item['card_product_name'] =
$result[0]['card_product_name'];
}
}
}
}
}// Dummy receipt
$receipt = [
'items' => [
[
'payment_type' => 2
]
]
];
$p = new Plugin(); // Database is not injected
$gen = $p->populateCreditCardData($receipt);
foreach ($gen as $effect) {
// Call $db here instead of injecting it.
// But now I have to propagate the $gen logic all over the call stack,
with "yield from"? :(
// Effect handlers solve this by forcing an effect up in the stack
trace similar to exceptions.// Dummy db result $rows = [ [ 'card_product_name' => 'KLARNA', ] ]; $gen->send($rows);
}
// Receipt item now has card_product_name populated properly.
print_r($receipt);
OK, so the problem with above code is that, in order for it to work, you
have to add "yield from" from the top to the bottom of the call stack,
polluting the code-base similar to what happens with "async" in JavaScript.
Also see the "Which color is your function" article [1].For this design pattern to work seamlessly, there need to be a way to yield
"all the way", so to speak, similar to what an exception does, and how
effect handlers work in OCaml [2].The question is, would this be easy, hard, or very hard to add to the
current PHP source code? Is it conceptually too different from generators?
Would it be easier to add a way to "jump back" from a catched exception
(kinda abusing the exception use-case, but that's how effect handlers work,
more or less)?Thanks for reading :)
Olle
Algebraic effects is a... big and interesting topic. :-) If we were to go that route, though, I would want to see something more formal than just a "yield far." That's basically another kind of unchecked exception, whereas I want us to move more toward checked exceptions.
--Larry Garfield
Den mån 16 juni 2025 kl 20:11 skrev Larry Garfield larry@garfieldtech.com:
Hello Internals,
I was pondering a little about effect handlers today, and how they could
work as a replacement for dependency injection and mocking. Let me show
an
example:<?php
require_once("vendor/autoload.php");
use Latitude\QueryBuilder\Engine\MySqlEngine;
use Latitude\QueryBuilder\QueryFactory;
use function Latitude\QueryBuilder\field;// Dummy db connection
class Db
{
public function getQueryBuilder()
{
return new QueryFactory(new MySqlEngine());
}
}interface Effect {}
class QueryEffect implements Effect
{
public $query;public function __construct($query) { $this->query = $query; }
}
class Plugin
{
/* The "normal" way to do testing, by injecting the db object. Not
needed here.
public function __construct(Db $db)
{
$this->db = $db;
}
*/public function populateCreditCardData(&$receipt) { foreach ($receipt['items'] as &$item) { // 2 = credit card if ($item['payment_type'] == 2) { $query = $this->db->getQueryBuilder() ->select('card_product_name ') ->from('card_transactions')
->where(field('id')->eq($item['card_transaction_id']))
->compile(); // Normal way: Call the injected dependency class
directly.
//$result = $this->db->search($query->sql(),
$query->params());
// Generator way, push the side-effect up the stacktrace
using generators.
$result = yield new QueryEffect($query);
if ($result) {
$item['card_product_name'] =
$result[0]['card_product_name'];
}
}
}
}
}// Dummy receipt
$receipt = [
'items' => [
[
'payment_type' => 2
]
]
];
$p = new Plugin(); // Database is not injected
$gen = $p->populateCreditCardData($receipt);
foreach ($gen as $effect) {
// Call $db here instead of injecting it.
// But now I have to propagate the $gen logic all over the call
stack,
with "yield from"? :(
// Effect handlers solve this by forcing an effect up in the stack
trace similar to exceptions.// Dummy db result $rows = [ [ 'card_product_name' => 'KLARNA', ] ]; $gen->send($rows);
}
// Receipt item now has card_product_name populated properly.
print_r($receipt);
OK, so the problem with above code is that, in order for it to work, you
have to add "yield from" from the top to the bottom of the call stack,
polluting the code-base similar to what happens with "async" in
JavaScript.
Also see the "Which color is your function" article [1].For this design pattern to work seamlessly, there need to be a way to
yield
"all the way", so to speak, similar to what an exception does, and how
effect handlers work in OCaml [2].The question is, would this be easy, hard, or very hard to add to the
current PHP source code? Is it conceptually too different from
generators?
Would it be easier to add a way to "jump back" from a catched exception
(kinda abusing the exception use-case, but that's how effect handlers
work,
more or less)?Thanks for reading :)
Olle
Algebraic effects is a... big and interesting topic. :-) If we were to go
that route, though, I would want to see something more formal than just a
"yield far." That's basically another kind of unchecked exception, whereas
I want us to move more toward checked exceptions.--Larry Garfield
I agree, and I was surprised to see OCaml going towards untyped effect
handlers, compared to, say, what they have in Koka [1].
I tried with Fiber::suspend(new QueryEffect($query)); and it works just
fine, but the intentionality of the code is a bit weak. I guess one could
just wrap it to make its purpose more clear, like
function query($query)
{
return Fiber::suspend(new QueryEffect($query));
}
// Inside fiber
// Query building logic omitted...
$rows = query($query); // Yield to top-level effect handler
Commitment to this design pattern is pretty high, since it's not contained
within a class or module. One could say the same about DI, perhaps. ;)
Anyway, this topic can continue somewhere else. Thanks for the feedback!
Olle
[1] - https://koka-lang.github.io/koka/doc/book.html#why-effects
Hello Internals,
I was pondering a little about effect handlers today, and how they could
work as a replacement for dependency injection and mocking. Let me show an
example:<?php
require_once("vendor/autoload.php");
use Latitude\QueryBuilder\Engine\MySqlEngine;
use Latitude\QueryBuilder\QueryFactory;
use function Latitude\QueryBuilder\field;// Dummy db connection
class Db
{
public function getQueryBuilder()
{
return new QueryFactory(new MySqlEngine());
}
}interface Effect {}
class QueryEffect implements Effect
{
public $query;public function __construct($query) { $this->query = $query; }
}
class Plugin
{
/* The "normal" way to do testing, by injecting the db object. Not
needed here.
public function __construct(Db $db)
{
$this->db = $db;
}
*/public function populateCreditCardData(&$receipt) { foreach ($receipt['items'] as &$item) { // 2 = credit card if ($item['payment_type'] == 2) { $query = $this->db->getQueryBuilder() ->select('card_product_name ') ->from('card_transactions') ->where(field('id')->eq($item['card_transaction_id'])) ->compile(); // Normal way: Call the injected dependency class directly. //$result = $this->db->search($query->sql(),
$query->params());
// Generator way, push the side-effect up the stacktrace
using generators.
$result = yield new QueryEffect($query);
if ($result) {
$item['card_product_name'] =
$result[0]['card_product_name'];
}
}
}
}
}// Dummy receipt
$receipt = [
'items' => [
[
'payment_type' => 2
]
]
];
$p = new Plugin(); // Database is not injected
$gen = $p->populateCreditCardData($receipt);
foreach ($gen as $effect) {
// Call $db here instead of injecting it.
// But now I have to propagate the $gen logic all over the call stack,
with "yield from"? :(
// Effect handlers solve this by forcing an effect up in the stack
trace similar to exceptions.// Dummy db result $rows = [ [ 'card_product_name' => 'KLARNA', ] ]; $gen->send($rows);
}
// Receipt item now has card_product_name populated properly.
print_r($receipt);
OK, so the problem with above code is that, in order for it to work, you
have to add "yield from" from the top to the bottom of the call stack,
polluting the code-base similar to what happens with "async" in JavaScript.
Also see the "Which color is your function" article [1].For this design pattern to work seamlessly, there need to be a way to yield
"all the way", so to speak, similar to what an exception does, and how
effect handlers work in OCaml [2].The question is, would this be easy, hard, or very hard to add to the
current PHP source code? Is it conceptually too different from generators?
Would it be easier to add a way to "jump back" from a catched exception
(kinda abusing the exception use-case, but that's how effect handlers work,
more or less)?Thanks for reading :)
Olle
Algebraic effects is a... big and interesting topic. :-) If we were to go that route, though, I would want to see something more formal than just a "yield far." That's basically another kind of unchecked exception, whereas I want us to move more toward checked exceptions.
--Larry Garfield
I think this might be entirely possible via an extension... there might need to be some hooks added to php-src but nothing that would require an RFC.
— Rob