Collection Builders

Robo provides task collections as a means of making error detection and recovery easier. When Robo tasks are added to a collection, their execution is deferred until the $collection->run() method is called. If one of the tasks fail, then the operation will be aborted; rollback tasks may also be defined to restore the system to its original condition.

When using collections, a Robo script will go through three phases:

  1. Determine which tasks will need to be run, and create a task builder.
  2. Assign values to variables.
  3. Do not alter the state of the system.
  4. Create the necessary tasks via the task builder.
  5. Use variables calculated in the first phase in task parameters.
  6. Run the tasks via the run() method.
  7. Check and report errors once after run() returns.

Following this pattern will keep your code linear and easy to understand.

Collections API

Collections are made up of a combination of tasks and/or callable functions / method pointers, such as:

Examples of adding different kinds of tasks to a collection are provided below.

TaskInterface Objects

<?php
  $collection->add(
    $this->taskExec('ls')
  );
?>

Functions

<?php
  $collection->addCode('mytaskfunction');
?>

Closures

<?php
  $collection->addCode(
    function() use ($work)
    {
      // do something with $work      
    });
?>

Methods

<?php
  $collection->addCode([$myobject, 'mymethod']);
?>

Using a Collection Builder

To manage a collection of tasks, a collection builder. Collection builders allow tasks to be created via chained methods. All of the tasks created by the same builder are added to a collection; when the run() method is called, all of the tasks in the collection run.

The 'publish' command from Robo's own RoboFile is shown below. It uses a collection builder to run some git and filesystem operations. The "completion" tasks are run after all other tasks complete, or during rollback processing when an operation fails.

<?php
class RoboFile extends \Robo\Tasks
{
    public function publish()
    {
        $current_branch = exec('git rev-parse --abbrev-ref HEAD');

        $collection = $this->collectionBuilder();
        $collection->taskGitStack()
            ->checkout('site')
            ->merge('master')
        ->completion($this->taskGitStack()->checkout($current_branch))
        ->taskFilesystemStack()
            ->copy('CHANGELOG.md', 'docs/changelog.md')
        ->completion($this->taskFilesystemStack()->remove('docs/changelog.md'))
        ->taskExec('mkdocs gh-deploy');

        return $collection;
    }
}
?>

The example above also adds a couple of tasks as "completions"; these are run when the collection completes execution, as explained below.

Rollbacks and Completions

Robo also provides rollbacks and completions, special tasks that are eligible to run only if all of the tasks added to the collection before them succeed. The section below explains the circumstances under which these tasks will run.

Completion Tasks

Completions run whenever their collection completes or fails, but only if all of the tasks that come before it succeed. An example of this is shown in the first example above. A filesystem stack task copies CHANDELOG.md to docs/changelog.md; after this task is added to the collection, another filesystem stack task is added as a completion to delete docs/changelog.md. This is done because docs/changelog.md is only intended to exist long enough to be used by the mkdocs task, which is added later.

Rollback Tasks

In addition to completions, Robo also supports rollbacks. Rollback tasks can be used to clean up after failures, so the state of the system does not change when execution is interrupted by an error. A rollback task is executed if all of the tasks that come before it succeed, and at least one of the tasks that come after it fails. If all tasks succeed, then no rollback tasks are executed.

Rollback and Completion Methods

Any task may also implement \Robo\Contract\RollbackInterface; if this is done, then its rollback() method will be called if the task is run() on a collection that later fails.

Use addAsCompletion($collection) in place of addAsRollback($collection), or implement \Robo\Contract\CompletionInterface. Completions otherwise work exactly like rollbacks.

Temporary Objects

Since the concept of temporary objects that are cleaned up on failure is a common pattern, Robo provides built-in support for them. Temporary directories and files are provided out of the box; other kinds of temporary objects can be easily created using the Temporary global collection.

Temporary Directories

It is recommended that operations that perform multiple filesystem operations should, whenever possible, do most of their work in a temporary directory. Temporary directories are created by $this->taskTmpDir(), and are automatically be removed when the collection completes or rolls back. As an added convenience, the CollectionBuilder class has a tmpDir() method that creates a temporary directory via taskTmpDir(), and then returns the path to the temporary directory.

<?php
class RoboFile extends \Robo\Tasks
{
    function myOperation()
    {
        $collection = $this->collectionBuilder();

        // Create a temporary directory, and fetch its path.
        $work = $collection->tmpDir();

        $collection
          ->taskWriteToFile("$work/README.md")
            ->line('-----')
            ->line(date('Y-m-d').' Generated file: do not edit.')
            ->line('----');

        // If all of the preceding tasks succeed, then rename the temporary 
        // directory to its final name.
        $collection->taskFilesystemStack()
          ->rename($work, 'destination');

        return $collection->run();
    }
}
?>

In the previous example, the path to the temporary directory is stored in the variable $work, and is passed as needed to the parameters of the other tasks as they are added to the collection. After the task collection is run, the temporary directory will be automatically deleted. In the example above, the temporary directory is renamed by the last task in the collection. This allows the working directory to persist; the collection will still attempt to remove the working directory, but no errors will be thrown if it no longer exists in its original location. Following this pattern allows Robo scripts to easily and safely do work that cleans up after itself on failure, without introducing a lot of branching or additional error recovery code. This paradigm is common enough to warrant a shortcut method of accomplishing the same thing. The example below is identical to the one above, save for the fact that it uses the workDir() method instead of tmpDir(). workDir() renames the temporary directory to its final name if the collection completes; any directory that exists in the same location will be overwritten at that time, but will persist if the collection roles back.

<?php
class RoboFile extends \Robo\Tasks
{
    function myOperation()
    {
        $collection = $this->collectionBuilder();

        // Create a temporary directory, and fetch its path.
        // If all of the tasks succeed, then rename the temporary directory
        // to its final name.
        $work = $collection->workDir('destination');

        $collection
          ->taskWriteToFile("$work/README.md")
            ->line('-----')
            ->line(date('Y-m-d').' Generated file: do not edit.')
            ->line('----');

        return $collection->run();
    }
}
?>

Temporary directories may also be created via the shortcut $this->_tmpDir();. Temporary directories created in this way are deleted when the script terminates.

Temporary Files

Robo also provides an API for creating temporary files. They may be created via $this->taskTmpFile(); they are used exactly like $this->taskWrite(), except they are given a random name on creation, and are deleted when their collection completes. If they are not added to a collection, then they are deleted when the script terminates.

The Temporary Global Collection

Robo maintains a special collection called the Temporary global collection. This collection is used to keep track of temporary objects that are not part of any collection. For example, Robo temporary directories and temporary files are managed by the Temporary global collection. These temporary objects are cleaned up automatically when the script terminates.

It is easy to create your own temporary tasks that behave in the same way as the provided temporary directory and temporary file tasks. There are two steps required:

For example, the implementation of taskTmpFile() looks like this:

<?php
    protected function taskTmpFile($filename = 'tmp', $extension = '', $baseDir = '', $includeRandomPart = true)
    {
        return Temporary::wrap(new TmpFile($filename, $extension, $baseDir, $includeRandomPart));
    }
?>

The complete() method of the task will be called once the Collection the temporary object is attached to finishes running. If the temporary is not added to a collection, then its complete() method will be called when the script terminates.

Named Tasks

It is also possible to provide names for the tasks added to a collection. This has two primary benefits:

  1. Any result data returned from a named task is stored in the Result object under the task name.
  2. It is possible for other code to add more tasks before or after any named task.

This feature is useful if you have functions that create task collections, and return them as a function results. The original caller can then use the $collection->before() or $collection->after() to insert sequenced tasks into the set of operations to be performed. One reason this might be done would be to define a base set of operations to perform (e.g. in a deploy), and then apply modifications for other environments (e.g. dev or stage).

<?php
  $collection->addCode(
    function() use ($work)
    {
      // do something with $work      
    },
    "taskname");
?>

Given a collection with named tasks, it is possible to insert more tasks before or after a task of a given name.

<?php
  $collection->after("taskname",
    function() use ($work)
    {
      // do something with $work after "taskname" executes, if it succeeds.    
    });
?>
<?php
  $collection->before("taskname",
    function() use ($work)
    {
      // do something with $work before "taskname" executes.    
    });
?>

It is recommended that named tasks be avoided unless specifically needed.