April 28, 2010

Rest Easy! A P4 Web Service in 50 LOC

Integration
Traceability

I am not a big fan of terse code, but it is cool when you can get a lot done with very little. I remember when I first started at Perforce a colleague of mine bet me that he could write the game "Worm" in 30 lines of Perl. This I had to see! A week later he had it done. It wasn't pretty, but it was impressive and well worth the six-pack of beer that I lost to the cause.

This article is not nearly as impressive, but I thought it would be a fun way to demo P4PHP, our newest derived API. If you are not familiar with Web Services, they are a really handy way of interacting with systems over a network. REST, or Representational State Transfer takes advantage of the fact that HTTP has built-in operations (GET, POST, PUT and DELETE) and uses them to perform actions on resources (identified by URIs). If you would like more information about RESTful Web Services, Wikipedia's Web Service and Representation State Transfer pages are a good place to start:

Step 1. Get PHP + Apache

For simplicity, I am going to assume you are using Ubuntu. In principle the steps are the same for other platforms, but if you are on OS X, you can probably skip this part (OS X ships with PHP and Apache out of the box).

  1. The following commands should install all of the required bits:
    $ sudo apt-get update
    $ sudo apt-get install apache2 libapache2-mod-php5 \
      php5 php5-cli php5-dev build-essential
  2. Now, let's make sure that mod-php is enabled and restart Apache for good measure:
    $ sudo a2enmod php5
    $ sudo /etc/init.d/apache2 restart
  3. To test your setup, remove /var/www/index.html and create /var/www/index.php with the following contents:
    <?php phpinfo(); ?>
    Now, open your web browser to http://localhost/. You should see a page with a bunch of information about your environment. If you have any trouble at this stage, there are many good tutorials out there with more detailed instructions.

Step 2. Get P4PHP

This part can be a bit trickier as it requires building from source. Remember, I am assuming you are using Ubuntu. You might need to adjust some of the commands if you are on a different platform.

  1. Make a directory to contain the P4PHP build and go there:
    $ mkdir ~/p4php-build
    $ cd ~/p4php-build
  2. Download P4PHP:
    $ wget \
      ftp://ftp.perforce.com/perforce/r09.2/bin.tools/p4php.tgz
  3. Download the correct P4API for your platform (e.g. 'darwin80u' for OS X):
    $ wget \
      ftp://ftp.perforce.com/perforce/r09.2/bin.linux26x86_64/p4api.tgz
  4. Unpack P4PHP and P4API:
    $ tar -xzf p4api.tgz
    $ tar -xzf p4php.tgz
  5. Build P4PHP. Note, the exact names of the directories will vary based on which build you happen to get:
    $ cd p4php-2009.2.228098
    $ phpize
    $ ./configure --with-perforce=../p4api-2009.2.228098/
    $ make
    $ sudo make install
  6. Enable the 'perforce' extension in your php.ini file:
    $ sudo sh -c "echo 'extension=perforce.so' > \
      /etc/php5/conf.d/perforce.ini"
  7. Verify the extension is loaded:
    $ php --ri perforce

Step 3. Setup Perforce

This script needs a Perforce Server to connect to and I strongly suggest that you use a test server rather than your production instance. If you don't already have a server that you can play with, try The Perforce Sample Depot.

Step 4. Configure Apache

In order to make use of 'pretty' URIs (instead of cramming everything into a query string), we need to turn on mod-rewrite in Apache. We also need to tweak the configuration to allow .htaccess overrides:

  1. Enable mod-rewrite:
    $ sudo a2enmod rewrite
  2. Edit '/etc/apache2/sites-available/default' to permit rewrite rules in .htaccess files. Under the '/var/www/' directory section, change "AllowOverride None" to "AllowOverride FileInfo".
  3. Create /var/www/.htaccess with the following contents:
    RewriteEngine On
    RewriteBase /
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^.*$ index.php [L]
  4. Restart Apache
    $ sudo /etc/init.d/apache2 restart

Step 5. Write the Darn Thing!

Now for the fun part! This is where the 50 lines come in. As you will see the bulk of the work was getting your environment setup. Now that we have a machine with PHP, P4PHP, Apache and Perforce, it's ridiculously easy to start exposing Perforce resources as web services.

  1. Bootstrap

    The first part of the script verifies that the Perforce extension is installed and establishes a connection to your Perforce Server. Make sure you set the connection parameters as appropriate. Note that maxlocktime and maxresults have been set conservatively to prevent runaway queries.

    1   <?php
    2   // ensure P4PHP is installed
    3   if (!extension_loaded('perforce')) {
    4       die(json_encode(array("error" => "P4PHP not found.")));
    5   }
    6   
    7   // connect to Perforce
    8   $p4              = new P4;
    9   $p4->port        = 'localhost:6666';
    10  $p4->user        = 'jdoe';
    11  $p4->ticket      = null;
    12  $p4->maxlocktime = 5000;
    13  $p4->maxresults  = 10000;
    14  $p4->connect();
  2. Request Routing

    The second phase of the script routes incoming HTTP requests to functions that you define. The behaviour is intentionally simple. Each request is examined and a few key variables are extracted:

       VERB  ->  The HTTP request method.
       NOUN  ->  The first URI path segment.
         ID  ->  Any remaining path segments.
    These variables are then combined to form a function call based on a strict convention. The verb and noun form the name of function, while the id is passed as the first argument to the function (the second argument is the 'p4' instance that we created during bootstrap). To illustrate, consider the following request URI:
     http://your-host/changes/12345
    As a standard 'get' request this will translate to the following function call (note that the function name is camel-cased and prefixed with 'http'):
     httpGetChanges('12345', $p4);

    If the function is defined, it's return value will be serialized to JSON and returned to the HTTP client. If the function is not defined, an error will be output (also in JSON).

    15  // route request
    16  $path     = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
    17  $params   = explode('/', trim($path, '/'), 2);
    18  $verb     = strtolower($_SERVER['REQUEST_METHOD']);
    19  $noun     = isset($params[0]) ? $params[0] : null;
    20  $id       = isset($params[1]) ? $params[1] : null;
    21  $function = 'http' . ucfirst($verb) . ucfirst($noun);
    22  if (!function_exists($function)) {
    23      die(json_encode(array("error" => "Invalid request.")));
    24  }
    25  
    26  // dispatch request to function
    27  try {
    28      $result = call_user_func($function, $id, $p4);
    29  } catch (P4_Exception $e) {
    30      $result = array("error" => $e->getMessage());
    31  }
    32  
    33  // output result as JSON
    34  print json_encode($result);
  3. Response Functions

    The final portion of the script contains the response functions. To keep it under 50 lines, I have only include a few here that demonstrate the basics.

    35  // serve-up p4 info.
    36  function httpGetInfo($id, $p4) {
    37      return $p4->run('info');
    38  }
    39  
    40  // serve one or more changes.
    41  function httpGetChanges($id, $p4) {
    42      if ($id) return array($p4->run('change', '-o', $id));
    43      return $p4->run('changes');
    44  }
    45  
    46  // create a job.
    47  function httpPostJobs($id, $p4) {
    48      $p4->input = $_POST;
    49      return $p4->run('job', '-i');
    50  }

    As you add more response functions, bear in mind that with RESTful web services it is common practice to translate the semantics of the HTTP methods to CRUD operations like so:

        POST  ->  Create
         GET  ->  Retrieve
         PUT  ->  Update
      DELETE  ->  Delete

With a web service like this running on your network, you are only a few lines of JavaScript away from gluing Perforce into your company intranet. For example, you could use XHR to present a live feed of check-ins or report on open bugs in your projects. If you use this script, I'd like to hear about it! Please post your questions and experiences in the comments below.