Last active
November 27, 2017 22:02
-
-
Save nicolopignatelli/f34e5636df050cc9f9aaf7c54075e82a to your computer and use it in GitHub Desktop.
How to implement a service endpoint using a functional style and monadic response objects
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 | |
// just a factory method for a Result | |
function result(callable $f, ...$args): Result { | |
try { | |
$value = $f(...$args); | |
return new Success($value); | |
} catch(\Exception $e) { | |
return new Failure(new ExceptionStack($e)); | |
} | |
} | |
// Result is the interface implemented by Success, Failure and Aborted | |
interface Result { | |
// extract the value encapsulated in a result | |
public function extract(); | |
// execute the callable after a previous successful result | |
public function then(callable $f): Result; | |
// execute the callable after a previous failed result | |
public function orElse(callable $f): Result; | |
} | |
final class Success implements Result { | |
// encapsulated value (the $something in the example below) | |
private $value; | |
public function __construct($value) { | |
$this->value = $value; | |
} | |
// get the encapsulated value | |
public function extract() { return $this->value; } | |
// after a successful result, we execute the passed callable on the encapsulated value and wrapped in a new Result object | |
public function then(callable $f): Result { | |
return result($f, $this->value); | |
} | |
// since the result is successful, we skip orElse branches | |
function orElse(callable $f): Result { return $this; } | |
} | |
class Failure implements Result { | |
// Failure always encapsulates an ExceptionStack | |
private $exceptionStack; | |
public function __construct(ExceptionStack $exceptionStack) { | |
$this->exceptionStack = $exceptionStack; | |
} | |
// get the stack back | |
public function extract() { return $this->exceptionStack; } | |
// the previous step resulted in a Failure, so we skip the happy branch | |
public function then(callable $f): Result { return $this; } | |
// we execute the failure branch by calling the passed callable with the exception stack as an argument, wrapped in a new Result | |
// if the callable is successful, we simply return a new Aborted result | |
// if the callable results in a second failure, we stack the two failures together and return a new Aborted | |
public function orElse(callable $f): Result { | |
return result($f, $this->exceptionStack) | |
->then(function() { return new Aborted($this->exceptionStack); }) | |
->orElse(function(ExceptionStack $exceptionStack) { | |
return new Aborted($this->exceptionStack->merge($exceptionStack)); | |
}) | |
->extract(); | |
} | |
} | |
// once a Failure occurred, we skip every following step | |
final class Aborted extends Failure { | |
public function then(callable $f): Result { return $this; } | |
public function orElse(callable $f): Result { return $this; } | |
} | |
// just a representation of the exceptions occurred during the execution | |
final class ExceptionStack { | |
/** @var \Exception[] **/ | |
private $stack; | |
public function __construct(\Exception ...$exceptions) { | |
$this->stack = $exceptions; | |
} | |
public function merge(ExceptionStack $exceptionStack) { | |
return new ExceptionStack(...array_merge($this->stack, $exceptionStack->stack)); | |
} | |
public function toString(): string { | |
$string = ''; | |
foreach ($this->stack as $exception) { | |
$string .= $exception->getMessage() . PHP_EOL; | |
$string .= $exception->getTraceAsString() . PHP_EOL; | |
} | |
return $string; | |
} | |
} | |
// example implementation of a service endpoint | |
function addSomethingToMyAggregate(UuidInterface $aggregateUuid, string $nameOfSomething): Result | |
{ | |
$result = | |
// happy path, we load the aggregate, add a new something to it and persist the aggregate back | |
result(function() use ($aggregateUuid, $nameOfSomething) { | |
$myAggregateId = new MyAggregateId($aggregateUuid); | |
$maybeAggregate = $this->myAggregates->getById($myAggregateId); // returns a Maybe monad encapsulating the MyAggregate object | |
switch (true) { | |
case $maybeAggregate instanceof Nothing: | |
throw new MyAggregateNotFound($myAggregateId); | |
case $maybeAggregate instanceof Just: | |
$aggregate = $maybeAggregate->extract(); | |
$something = new Something(new Name($nameOfSomething)); | |
$aggregate->addSomething($something); | |
$this->aggregates->save($aggregate); | |
return $aggregate; | |
} | |
}) | |
// whoops, something went wrong, let's log what happened | |
->orElse(function(ExceptionStack $exceptionStack) { | |
$this->logger->error("Cannot add Something", ['exception' => $exceptionStack->toString()]); | |
}); | |
// at this point, if something went wrong before nothing will be executed | |
// but if everything went ok, we attempt secondary stuff like publishing events and logging about the successful call | |
$result | |
->then(function (Something $something) use ($aggregateUuid) { | |
$myAggregateId = new MyAggregateId($aggregateUuid); | |
$this->logger->info("Something was added", ['name' => $something->getName(), 'my_aggregate_id' => $myAggregateId]); | |
$this->publisher->publish( | |
new SomethingWasAdded($something, $myAggregateId), | |
SomethingWasAdded::class | |
); | |
}) | |
// if something goes wrong in the previous call, we log it again. | |
// Nevertheless, the aggregate was persisted and the $return value of the method will be Success. | |
// That's why we separated the first batch of the operations (critical for the service outcome) from this one. | |
->orElse(function (ExceptionStack $exceptionStack) { | |
$this->logger->warning("Something added with error", ['exception' => $exceptionStack->toString()]); | |
}); | |
return $result; | |
} | |
$result = addSomethingToMyAggregate(Uuid::v4(), "Name of something"); | |
switch (true) { | |
case $result instanceof Success: | |
echo 'Yeah! Added new something: ' . $result->extract()->getName(); | |
break; | |
case $result instanceof Failure: | |
echo 'Nooo, exception occurred: ' . $result->extract()->toString(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment