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.
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.