I've got a serious Not Invented Here syndrome problem when it comes to (database) migrations.

I've previously blogged about migrations with phing and dbdeploy and also porting ActiveRecord::Migrations to PHP, now here I am again blogging about yet another way of doing migrations in PHP projects. Only maybe this time it's different, maybe this time I've found a way I'm happy with...?

My requirements were:

  • Ability to run 'interleaved' migrations
  • Migrate/rollback
  • Not tied to any framework/library or particular way of configuring your app
  • Migrations must be able to execute code, plain SQL isn't enough - think data manipulation, or migrating XML files etc
  • Pretty console colours
  • Easy install

Phpmig is a simple migrations system that was written to be easily adopted regardless of the framework or libraries you are using. It requires a little bit of setting up, but if you know you should be using migrations, you're probably more than capable.

Phpmig status command

It's really simple

On it's own, it doesn't really do a lot. It runs migrations.

You have to tell it where your migrations are. You have to tell it where to record which migrations have been run. You do this by passing it something that implements the ArrayAccess interface, with at least two keys, phpmig.adapter and phpmig.migrations. The former needs to be an instance of Phpmig\Adapter\AdapterInterface, and allows phpmig to record and decide which migrations have been run. The latter is an array of migration files.

For this, I recommend using Pimple, a small service/dependency injection container, there's a version bundled under the Phpmig namespace, Phpmig\Pimple\Pimple. The nice thing is, the very same ArrayAccess object is accesible from your migration files. Phpmig doesn't provide you with a way of accessing your database, seeing as that's probably quite specific to your project, depending on the module you're working on, or the environment you're working on, you'll have to take care of it yourself.

Example Applicaiton

If you want to get stuck in straight away, checkout the README, otherwise read on for a simple usage example.

I'm going to use Silex as the base for my example app, for two reasons. Firstly it's really nice, second because it uses an instance of Pimple as a service container.

A little setup first:

$ cd ~/src
$ mkdir phpmig-example-app
$ cd phpmig-example-app
$ wget http://silex.sensiolabs.org/get/silex.phar
$ mkdir vendor
$ git submodule add https://github.com/doctrine/dbal vendor/doctrine-dbal
$ git submodule add https://github.com/doctrine/common vendor/doctrine-common

Now we can move on to writing our app. Rather than having what's essentially the bootstrap in the index file, I put everything in a file called app.php

<?php

// app.php

require_once __DIR__.'/silex.phar';

$app = new Silex\Application();

$app->get('/hello/{name}', function($name) use($app) {
    return 'Hello '.$app->escape($name);
});

$app->register(new Silex\Provider\DoctrineServiceProvider(), array(
    'db.options'            => array(
        'driver'    => 'pdo_sqlite',
        'path'      => __DIR__.'/app.db',
    ),
    'db.dbal.class_path'    => __DIR__.'/vendor/doctrine-dbal/lib',
    'db.common.class_path'  => __DIR__.'/vendor/doctrine-common/lib',
));

return $app;

The index file simply includes the app.php file and runs the app.

<?php 

// index.php

$app = include __DIR__.'/app.php';

$app->run();

That's our app built, so now we can phpmig it:

$ sudo pear channel-discover pear.atstsolutions.co.uk
$ sudo pear install atst/phpmig-alpha
$ cd ~/src/phpmig-example-app
$ phpmig init
+d ./migrations Place your migration files in here
+f ./phpmig.php Create services in here
$

Phpmig has created a phpmig bootstrap file for us, but it's very basic:

<?php

use \Phpmig\Adapter,
    \Phpmig\Pimple\Pimple;

$container = new Pimple();

$container['phpmig.adapter'] = $container->share(function() {
    // replace this with a better Phpmig\Adapter\AdapterInterface 
    return new Adapter\File\Flat(__DIR__ . DIRECTORY_SEPARATOR . 'migrations/.migrations.log');
});

$container['phpmig.migrations'] = function() {
    return glob(__DIR__ . DIRECTORY_SEPARATOR . 'migrations/*.php');
};

return $container;

We don't need to use phpmig's bundled version of pimple, and we've got ourselves a nice doctrine connection setup in our app, so we may as well use that to record the running of our migrations, rather than a flat file.

<?php

use \Phpmig\Adapter;

$container = include __DIR__.'/app.php';

$container['phpmig.adapter'] = $container->share(function($c) {
    return new Adapter\Doctrine\DBAL($c['db'], 'migrations');
});

$container['phpmig.migrations'] = function() {
    return glob(__DIR__ . DIRECTORY_SEPARATOR . 'migrations/*.php');
};

return $container;

We can see if things are working by running phpmig's status command:

$ phpmig status

 Status   Migration ID    Migration Name 
-----------------------------------------

We can now create our first migration. Phpmig will create a skeleton for us, but we have to give it a name and a place to put it

$ phpmig generate CreateFirstTable ./migrations
+f ./migrations/20111101000144_CreateFirstTable.php

The file it generates contains two empty methods, up and down.

<?php

use Phpmig\Migration\Migration;

class CreateFirstTable extends Migration
{
    /**
     * Do the migration
     */
    public function up()
    {

    }

    /**
     * Undo the migration
     */
    public function down()
    {

    }
}

As our service container has a db connection for us to use, we can use that to create (or drop) our table.

<?php

use Phpmig\Migration\Migration;

class CreateFirstTable extends Migration
{
    /**
     * Do the migration
     */
    public function up()
    {
        $c = $this->getContainer();
        $c['db']->query("
            CREATE TABLE `user` (
                `id` INTEGER,
                `name` TEXT,
                PRIMARY KEY (`id`)
            );
        ");
    }

    /**
     * Undo the migration
     */
    public function down()
    {
        $c = $this->getContainer();
        $c['db']->query("DROP TABLE IF EXISTS `user`;");
    }
}

All that's left now is to run the migration we've just created

$ phpmig status

 Status   Migration ID    Migration Name 
-----------------------------------------
   down  20111101000144  CreateFirstTable

$ phpmig migrate
 == 20111101000144 CreateFirstTable migrating
 == 20111101000144 CreateFirstTable migrated 0.0037s
$ phpmig status

 Status   Migration ID    Migration Name 
-----------------------------------------
     up  20111101000144  CreateFirstTable

$ 

Comments, Feedback and Patches

All are welcome, it's still in it's infancy, but I'm using it on a couple of projects already and I'm fairly happy with it. I've not created pear packages (or indeed run a pear server) before, so go easy on me if I've screwed anything on that side of things.