Console Output with Generators

A problem I’ve found with command line applications is that you often find you want to output messages to the console at various points during the application’s execution. However, if and when these messages are to be output is generally dictated by your application’s business logic. This very quickly leads to your business logic leaking into your CLI-specific application logic or vice-versa.

I took a shot at using generators to provide a clear separation between my business and application logic.

Generators

A generator is a function that uses the yield keyword to repeatedly return control to the caller.

function someGenerator()
{
  yield "first value";
  yield "second value";
}

The caller can treat the generator like it returns an iterator.

function someCaller()
{
  foreach (someGenerator() as $value) {
    // Do something with each yielded $value
  }
}

Payload DTO

I created a DTO to serve as the go-between for the layers, called Payload. This is based on the implementation of the Aura Payload package.

class Payload
{
  private $status;
  private $message;

  // Constructors
  // Getters
}

The status is domain-specific and it is up to the application to interpret what to do with different statuses. The message is just a string that the domain service yields.

Domain API

In an API object for the domain layer I created a generator that yields only Payload objects.

class DomainService
{
  public function call()
  {
    // Do something
    yield Payload::processing("A message to output");
    // Do something else
    if (/* Some condition */) {
      yield Payload::failure("Something went wrong");
    }
    // Do more things
    yield Payload::success("All good");
  }
}

The business logic treats yielding a failure Payload as an indication that execution of the service stops.

Application Layer

In the application layer the command iterates over the generator and outputs each message as it receives it. If a failure Payload is received it stops execution.

public function execute(InputInterface $input, OutputInterface $output)
{
  foreach ($this->domainService->call() as $payload) {

    $output->writeln($payload->getMessage());

    if ($payload->getStatus() === Payload::FAILED) {
      return self::EXIT_STATUS_FAILURE;
    }
  }

  return self::EXIT_STATUS_SUCCESS;
}

The domain’s API object can be used in non-CLI applications as well. In a HTTP controller action the messages from each Payload can be collected and output as the response body.

public function action(Request $request)
{
  $messages = [];

  foreach ($this->domainService->call() as $payload) {

    $messages[] = $payload->getMessage();

    if ($payload->getStatus() === Payload::FAILED) {
      return new Response(implode("\n", $messages), 500);
    }
  }

  return new Response(implode("\n", $messages), 200);
}

I’ve put together an example repository with a single domain service that is used by CLI and HTTP applications.

Further Possibilities

The Payload DTO isn’t limited to just a status and a message. The Aura Payload package has input, output and extras as additional attributes. Any data that needs to be handled by the application layer can be passed as part of the Payload. Different Payload classes can be created for different domain API objects.

The yield keyword can be used to accept input from the caller as well. This could be used in interactive CLI applications.

PHP 7 will introduce a yield from syntax that allows generators to delegate to other generators. This would make it simpler to have a domain API object composed of one or more other domain API objects.

PHP 7 will also introduce Generator Return Expressions. These may be able to make halting execution at a failure Payload more explicit, by replacing yield with return.