April 23, 2009

Bulk view

Drupal staging and deployment tips: It’s all code

As I talk to more and more developers about practices for working with Drupal, I get the idea that the staging and deployment process adopted by my team isn’t widespread.

Many developers make their changes directly through the web-based interface of a testing server, or even on the production site itself. I think that’s both tedious and scary. =)

Putting all of our behavior-related changes in update function in the install file makes it easier to merge changes and repeatedly test upgrades. Editorial changes (fixing typos, etc.) can happen on the site, but if it’s behavior-related code, it should be in the code repository. In moments of weakness, we’ve made web-based changes to our site, and we almost always regret those–either right away because things broke, or when we try to reconstruct our changes.

The Module Developer’s Guide documents how to write .install files for Drupal 5 and Drupal 6, but doesn’t go into much detail about what else you can put in the update functions. Maybe that’s why many developers use install files only for database-related changes. But you can do so much more: creating nodes, adding permissions, enabling other modules, and so on. 

Watch out for these potential pitfalls:

  • $user is set to the superuser (uid = 1) if you run update.php after logging in as the superuser, but if $access_check is set to FALSE in update.php, then $user will be null. That means that if you’re creating nodes or doing other things that check user_access or node_access, you should temporarily switch to the superuser.
  • If your update works if you apply them one at a time, but not if you apply all of them in one go, look for functions that cache information in static variables. You may need to modify the source code to add an argument that allows you to reset the cache or find another way to deal with the static variables. Static variables are a pain.
  • Another reason why batch updates may fail while incremental updates work has to do with the order that the update functions are called in. Modules are processed according to weight and alphabetical order, and all applicable update functions within one module are run before moving onto the next. If you have a set of related modules, put update functions that affect the related modules into the module with the heaviest weight.
  • .install files may get really long if you create _update_N functions for every change and you frequently deploy to a test server. You can refactor your update functions if other developers know that they should test the updates from a fresh copy of the production database instead of from incremental updates to their current system. Make sure you don’t add code below the update level on the production server.
  • Don’t forget to return an array containing results, or even just array().
  • Here are some general tips on how to find out the programmatic equivalent of a web-based action:

    • Find the form or form_submit function that processes the action and see if there are any API functions you can call to produce the same effect. If you can’t find an API function, consider writing one.
    • If you can’t write an API function, use the Macro module (part of the Devel module for Drupal 5) to record the form submission, and then use drupal_execute to run the recorded macro, OR
    • Directly manipulate the database, making sure to call any hooks necessary.

    And here are some examples of programmatically doing things:

    Setting a variable
    variable_set(‘yourvariable’, ‘yourvalue’);

    Enabling modules
    include_once(‘includes/install.inc’);
    module_rebuild_cache();
    drupal_install_modules(array(‘module1′, ‘module2′));

    Disabling modules
    db_query(“UPDATE {system} SET status=0 WHERE type=’module’ AND name=’%s'”, ‘modulename’);

    Creating nodes 
    global $user;
    $old_user = $user;
    $user = user_load(array(‘uid’ => 1));
    $session = session_save_session();
    $session_save_session(FALSE);
    // Do the work
    $node = new stdClass();
    $node->type = ‘page';
    $node->title = ‘Title';
    $node->body = ‘Body';
    node_save($node);
    // Restore the user
    $user = $old_user;
    session_save_session($session);

    Deleting nodes
    global $user;
    $old_user = $user;
    $user = user_load(array(‘uid’ => 1));
    $session = session_save_session();
    session_save_session(FALSE);
    // Do the work
    node_delete($nid);
    // Restore the user
    $user = $old_user;
    session_save_session($session);

    Updating the list of blocks
    global $theme_key;
    $theme_key = ‘yourtheme';
    _block_rehash();

    Convenience functions for working with permissions

    function _add_permissions($roles, $permissions) {
      $ret = array();
      foreach ($roles as $rid) {
        if (is_numeric($rid)) {
          $role = db_fetch_array(db_query("SELECT rid, name FROM {role} WHERE rid=%d",
    				      $rid));
        }
        else {
          $role = db_fetch_array(db_query("SELECT rid, name FROM {role} WHERE name='%s'",
    				      $rid));
        }
        $role_permissions =
          explode(', ',
    	      db_result(db_query('SELECT perm FROM {permission} WHERE rid=%d',
    				 $role['rid'])));
        $role_permissions = array_unique(array_merge($role_permissions, $permissions));
        db_query('DELETE FROM {permission} WHERE rid = %d', $role['rid']);
        db_query("INSERT INTO {permission} (rid, perm) VALUES (%d, '%s')",
    	     $role['rid'],
    	     implode(', ', $role_permissions));
        $ret[] = array('success' => true,
    		   'query' => "Added " . implode(', ', $permissions)
    		   . ' permissions for ' . $role['name']);
      }
      return $ret;
    }
    
    function _remove_permissions($roles, $permissions) {
      $ret = array();
      foreach ($roles as $rid) {
        if (is_numeric($rid)) {
          $role = db_fetch_array(db_query("SELECT rid, name FROM {role} WHERE rid=%d",
    				      $rid));
        }
        else {
          $role = db_fetch_array(db_query("SELECT rid, name FROM {role} WHERE name='%s'",
    				      $rid));
        }
        $role_permissions =
          explode(', ',
    	      db_result(db_query('SELECT perm FROM {permission} WHERE rid=%d',
    				 $role['rid'])));
        $role_permissions = array_diff($role_permissions, $permissions);
        db_query('DELETE FROM {permission} WHERE rid = %d', $role['rid']);
        db_query("INSERT INTO {permission} (rid, perm) VALUES (%d, '%s')",
    	     $role['rid'],
    	     implode(', ', $role_permissions));
        $ret[] = array('success' => true,
    		   'query' => "Removed " . implode(', ', $permissions)
    		   . ' permissions for ' . $role['name']);
      }
      return $ret;
    }
    

    Use update functions for all of your behavioral changes. Use source code control. Write regression tests. These practices won’t take all the challenges out of Drupal development, but they certainly make it less stressful–and more fun. =)

    Mapping what makes me happy

    Thinking about what makes you happy is a good way to tweak your life so that you do more of the things that make you happier.

    Here’s an incomplete, not-to-scale map of things that make me happy. I started by brainstorming lots of things, then moving them around in the Inkscape drawing program (it’s like magnetic poetry with an infinite refrigerator door!) until order emerged. Also, reading the book Back of the Napkin helped.

    I’ve divided into things I do with other people and things I do with myself, and I’ll add more as other things occur to me.

    Happiness map - click for full size

    What’s your happiness map? Here’s how you can figure it out:

    1. Take a whole bunch of sticky notes, index cards, or other things you can write ideas on.
    2. List all the things you do that you enjoy.
    3. Move them around until an order makes sense. I sorted mine in order of increasing happiness, and then I grouped them by type.