First there was ant, then there was grunt and gulp, now there's pulp.

I decided to get down and dirty with ReactPHP and make a project that really couldn't be made without that library. I decided to write a gulp clone in PHP. Sometimes requiring nodejs, npm, et. al. causes more trouble than it's worth. You can't tell me you've never got into a situation where npm install doesn't fail beause of permission issues - or the most recent version isn't available on this Ubuntu 14 or CentOs 6 machine you're required to use. Also, NodeJS projects are sometimes hard to really grok because the functionality is split amongst a half dozen projects or so. Tracing one method might involve googling 6 or 7 package names and traversing their github pages.

The Task

function compileLess($p) {                                                                                                                                            
    return $p->src(['public/templates/styles/my-project.less'])
      ->pipe(new \Pulp\Debug())                     //will simply print every file from src
      ->pipe(new \Pulp\Less( ['compress'=>TRUE] ))  //renames files from *.less to *.css
      ->pipe($p->dest('public/templates/styles/')); //will output public/templates/styles/my-project.css
}

$p->task('less-now', function() use($p) { 
    return compileLess($p);
});

The above code comes from the configuration file of Pulp where you build your own tasks. Tasks can simply be anonymous functions. The anonymous function wrapper and the implementation function are separated for clarity.

The $p variable is a global instance of the main Pulp object. It has 4 main methods:

  • task( name, [dep, dep], callback ):
  • src( [glob, glob], [key=>val]):
  • dest( [direcotry], [key=>val]):
  • watch( [glob, glob], [key=>val] ):

The task() method simply names a function and, optionally, defines a list of dependent tasks to run before the target task.

Each of the remaining methods returns a core sublcass of Pipe. src() is designed to iterate over directory and glob pattrens (like "src/**/*.js"). dest() can take an array of directories and it writes files piped to it to all of those directories. watch() is like src, but it checks the file stat for changed times before fireing a change event.

Notice that the pipe() method comes from the result of the src() and not from the global $p object. The pipe() method connects two DataPipe objects: one being the source the other being the destination. Files that are pushed by the source go to the destination. If a file is not pushed, then it is removed from the pipeline.

Data Pipes

The foundation of the data pipeline is the DataPipe class, which is an implementation of ReactPHP's Duplex Stream . It is designed around receiving and emitting a stream of files (SplFileInfo objects).


namespace Pulp;
use \Evenement\EventEmitterTrait;
use React\Stream\Util;
use React\Stream\WritableStreamInterface;

class DataPipe implements \React\Stream\DuplexStreamInterface { 

    use \Evenement\EventEmitterTrait;

    public $writeCallback;
    public $endCallback;
    protected $chunkList = [];

    public function __construct($writeCallable=NULL, $endCallable=NULL) { 
        $this->writeCallback = $writeCallable;
        $this->endCallback   = $endCallable;
    } 

    public function end($data=null) {
        $this->flush();

        $cb = $this->endCallback;
        if (is_array($cb)) {
            call_user_func($cb, $this);
        } else if ($cb) {
            $cb($this);
        }

        $this->close();
        $this->emit('end');
    }

    public function write($data) {
        $cb = $this->writeCallback;
        if (is_array($cb)) {
            call_user_func($cb, $data, $this);
        } else if ($cb) {
            $cb($data, $this);
        }
        $this->_onWrite($data);
    }

    public function pipe(WritableStreamInterface $dest, array $options = array()) {
        return Util::pipe($this, $dest, $options);
    }
}

Some non-essential code has been removed for clarity.

A pipe can have two callbacks: a write callback and an end callback (the end callback is not often used).

You can create ad-hoc pipes to rapidly create your build pipeline without committing to maintaining full plugins.

function compileLess($p) {                                                                                                                                            
    return $p->src(['public/templates/styles/my-project.less'])
      ->pipe(new \Pulp\Debug())                     //will print every file from src
      ->pipe(new \Pulp\DataPipe(function($file) {   //does the same thing as Debug()
          echo "pulp-debug: ". $file."\n";
      }))                  
}

But, you must remember to push your files down the pipeline when you're done with them.

function compileLess($p) {                                                                                                                                            
    return $p->src(['public/templates/styles/my-project.less'])
      ->pipe(new \Pulp\DataPipe(function($file, $pipe) {   //accept the pipe itself as second parameter
          echo "pulp-debug: ". $file."\n";
          $pipe->push($file);                              //without this, no further pipe will get files from src
      }))                  
      ->pipe(new \Pulp\Debug())                            //now Debug() will also get the files.
}

Keeping your project clean

I really liked this idea from altax so I borrowd it for this project. Altax keep all the configuration files in a hidden subdirectory.


your-project/
  .pulp/            # <== created with pulp init-project
     config.php     # <== tasks go here
     composer.json  # <== pulp plugins go here
  src/
    controllers/
  public/
    templates/
      styles/
        my-project.less

I've noticed that some projects don't leave any "room" in their repos to put supporting build scripts (i.e. the top level dir contains the publicly accessible front controller).

Yeah, you're looking at that correctly. cd into .pulp/ and run composer install to keep build dependencies separate from regular project dependencies (including regular dev-dependencies).

It's simple, really

And that's all there is to it. ReactPHP's streams make it really simple to build this kind of system on top.

Go check it out: Github Page

Github Project Page