Pre-req: Functional core, imperative shell
Pre-req: Side-effects, purity, referential transparency
- The functional core is more testable and more composable than the imperative shell or effectful code
- Extending the functional core means making a bigger part of the software pure
- Side-effects can in some cases easily be lifted out from a function, creating a more composable and testable unit
- But sometimes, side-effects are tangled in business logic
- Some side-effects can be delayed until end of request (e.g., updating user's name in database; exception at failure can still be thrown)
- Example: Create dummy users
/**
* Alternatives:
*
* 1) Naive (implicit depedencies, everything must be integrity tested)
* 2) Injected implementation (factories)
* 3) Queue implementation (this implementation; side-effects are delayed or "delegated" to the queue)
*/
function actionCreateDummyUsers(Request $request)
{
$times = $request->getParam('times', 5);
$prefix = $request->getParam('prefix', 'randuser_');
$email = $request->getParam('email', '[email protected]');
$randomUsers = [];
for (; $times > 0; $times--) {
que(
function (PasswordManagement $pwm, User $user) use ($prefix, $email, &$randomUsers) {
$password = $pwm->getRandomPassword();
$name = $user->getRandomUsername($prefix);
$user->users_name = $name;
$user->full_name = $name;
$user->email = $email;
$user->created = date('Y-m-d H:i:s');
$user->modified = date('Y-m-d H:i:s');
$user->password = password_hash($password, PASSWORD_DEFAULT);
if ($user->save()) {
$randomUsers[] = ['username' => $name, 'password' => $password];
}
}
);
}
return [
'success' => true,
'randomUsers' => &$randomUsers,
'filename' => $prefix
];
}
// Usage:
$result = actionCreateDummyUsers(new Request());
$queue = que(null);
$fn = $queue->pop();
$fn(new PasswordManagement(), new User());
var_export($result);
/*
array (
'success' => true,
'randomUsers' =>
array (
0 =>
array (
'username' => '1abc',
'password' => '123',
),
),
'filename' => 1,
)
*/
// Simple queue class
class Queue
{
/** @var callable[] */
private $fns = [];
public function __construct(array $fns)
{
$this->fns = $fns;
}
// Run all callables
// TODO: Autowire with reflection
public function run()
{
array_walk($this->fns, fn ($fn) => $fn());
}
public function pop()
{
return array_pop($this->fns);
}
}
// Simple que() function
function que(callable|null $fn)
{
static $fns = [];
if (is_callable($fn)) {
$fns[] = $fn;
} else {
return new Queue($fns);
}
}
// Dummy user, save() must be mocked in unit-test
class User
{
public $id;
public function getRandomUsername(string $prefix)
{
return $prefix . 'abc';
}
public function save()
{
return true;
}
}
// Dummy request
class Request
{
public function getParam($name, $default)
{
return 1;
}
}
// Dummy class
class PasswordManagement
{
public function getRandomPassword()
{
return '123';
}
}
Related: EventSourcing: https://martinfowler.com/eaaDev/EventSourcing.html