Skip to content

Instantly share code, notes, and snippets.

@nicolopignatelli
Last active November 27, 2017 22:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nicolopignatelli/f34e5636df050cc9f9aaf7c54075e82a to your computer and use it in GitHub Desktop.
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
<?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