--- /dev/null
+<?php
+namespace Robo\Task;
+
+use Robo\Result;
+
+/**
+ * Extend StackBasedTask to create a Robo task that
+ * runs a sequence of commands.
+ *
+ * This is particularly useful for wrapping an existing
+ * object-oriented API. Doing it this way requires
+ * less code than manually adding a method for each wrapped
+ * function in the delegate. Additionally, wrapping the
+ * external class in a StackBasedTask creates a loosely-coupled
+ * interface -- i.e. if a new method is added to the delegate
+ * class, it is not necessary to update your wrapper, as the
+ * new functionality will be inherited.
+ *
+ * For example, you have:
+ *
+ * $frobinator = new Frobinator($a, $b, $c)
+ * ->friz()
+ * ->fraz()
+ * ->frob();
+ *
+ * We presume that the existing library throws an exception on error.
+ *
+ * You want:
+ *
+ * $result = $this->taskFrobinator($a, $b, $c)
+ * ->friz()
+ * ->fraz()
+ * ->frob()
+ * ->run();
+ *
+ * Execution is deferred until run(), and a Robo\Result instance is
+ * returned. Additionally, using Robo will covert Exceptions
+ * into RoboResult objects.
+ *
+ * To create a new Robo task:
+ *
+ * - Make a new class that extends StackBasedTask
+ * - Give it a constructor that creates a new Frobinator
+ * - Override getDelegate(), and return the Frobinator instance
+ *
+ * Finally, add your new class to loadTasks.php as usual,
+ * and you are all done.
+ *
+ * If you need to add any methods to your task that should run
+ * immediately (e.g. to set parameters used at run() time), just
+ * implement them in your derived class.
+ *
+ * If you need additional methods that should run deferred, just
+ * define them as 'protected function _foo()'. Then, users may
+ * call $this->taskFrobinator()->foo() to get deferred execution
+ * of _foo().
+ */
+abstract class StackBasedTask extends BaseTask
+{
+ /**
+ * @var array
+ */
+ protected $stack = [];
+
+ /**
+ * @var bool
+ */
+ protected $stopOnFail = true;
+
+ /**
+ * @param bool $stop
+ *
+ * @return $this
+ */
+ public function stopOnFail($stop = true)
+ {
+ $this->stopOnFail = $stop;
+ return $this;
+ }
+
+ /**
+ * Derived classes should override the getDelegate() method, and
+ * return an instance of the API class being wrapped. When this
+ * is done, any method of the delegate is available as a method of
+ * this class. Calling one of the delegate's methods will defer
+ * execution until the run() method is called.
+ *
+ * @return null
+ */
+ protected function getDelegate()
+ {
+ return null;
+ }
+
+ /**
+ * Derived classes that have more than one delegate may override
+ * getCommandList to add as many delegate commands as desired to
+ * the list of potential functions that __call() tried to find.
+ *
+ * @param string $function
+ *
+ * @return array
+ */
+ protected function getDelegateCommandList($function)
+ {
+ return [[$this, "_$function"], [$this->getDelegate(), $function]];
+ }
+
+ /**
+ * Print progress about the commands being executed
+ *
+ * @param string $command
+ * @param string $action
+ */
+ protected function printTaskProgress($command, $action)
+ {
+ $this->printTaskInfo('{command} {action}', ['command' => "{$command[1]}", 'action' => json_encode($action, JSON_UNESCAPED_SLASHES)]);
+ }
+
+ /**
+ * Derived classes can override processResult to add more
+ * logic to result handling from functions. By default, it
+ * is assumed that if a function returns in int, then
+ * 0 == success, and any other value is the error code.
+ *
+ * @param int|\Robo\Result $function_result
+ *
+ * @return \Robo\Result
+ */
+ protected function processResult($function_result)
+ {
+ if (is_int($function_result)) {
+ if ($function_result) {
+ return Result::error($this, $function_result);
+ }
+ }
+ return Result::success($this);
+ }
+
+ /**
+ * Record a function to call later.
+ *
+ * @param string $command
+ * @param array $args
+ *
+ * @return $this
+ */
+ protected function addToCommandStack($command, $args)
+ {
+ $this->stack[] = array_merge([$command], $args);
+ return $this;
+ }
+
+ /**
+ * Any API function provided by the delegate that executes immediately
+ * may be handled by __call automatically. These operations will all
+ * be deferred until this task's run() method is called.
+ *
+ * @param string $function
+ * @param array $args
+ *
+ * @return $this
+ */
+ public function __call($function, $args)
+ {
+ foreach ($this->getDelegateCommandList($function) as $command) {
+ if (method_exists($command[0], $command[1])) {
+ // Otherwise, we'll defer calling this function
+ // until run(), and return $this.
+ $this->addToCommandStack($command, $args);
+ return $this;
+ }
+ }
+
+ $message = "Method $function does not exist.\n";
+ throw new \BadMethodCallException($message);
+ }
+
+ /**
+ * @return int
+ */
+ public function progressIndicatorSteps()
+ {
+ // run() will call advanceProgressIndicator() once for each
+ // file, one after calling stopBuffering, and again after compression.
+ return count($this->stack);
+ }
+
+ /**
+ * Run all of the queued objects on the stack
+ *
+ * @return \Robo\Result
+ */
+ public function run()
+ {
+ $this->startProgressIndicator();
+ $result = Result::success($this);
+
+ foreach ($this->stack as $action) {
+ $command = array_shift($action);
+ $this->printTaskProgress($command, $action);
+ $this->advanceProgressIndicator();
+ // TODO: merge data from the result on this call
+ // with data from the result on the previous call?
+ // For now, the result always comes from the last function.
+ $result = $this->callTaskMethod($command, $action);
+ if ($this->stopOnFail && $result && !$result->wasSuccessful()) {
+ break;
+ }
+ }
+
+ $this->stopProgressIndicator();
+
+ // todo: add timing information to the result
+ return $result;
+ }
+
+ /**
+ * Execute one task method
+ *
+ * @param string $command
+ * @param string $action
+ *
+ * @return \Robo\Result
+ */
+ protected function callTaskMethod($command, $action)
+ {
+ try {
+ $function_result = call_user_func_array($command, $action);
+ return $this->processResult($function_result);
+ } catch (\Exception $e) {
+ $this->printTaskError($e->getMessage());
+ return Result::fromException($this, $e);
+ }
+ }
+}