Develop Simply

Ivan K's development musings

Setting Up Bad Ass File Uploading

So you just want to upload files, maybe resize some images, maybe even do some validation - well you’re in luck. This quick tutorial will show you how to do this the easy way with Jam.

First off, lets create a table for our model. We’ll be using timestamped-migrations for that:

minion db:generate --name=create_table_paintings
> 1351501477_create_table_paintings.php Migration File Generated

Now we can modify the migration file to add the nesessary fields:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php defined('SYSPATH') OR die('No direct script access.');

class Create_Table_Paintings extends Migration
{
  public function up()
  {
      $this->create_table('paintings', array(
          'name' => 'string',
          'file' => 'string',
      ));
  }
  
  public function down()
  {
      $this->drop_table('paintings');
  }
}
?>

The id column will be added automatically. You can disable that behavior but its generally useful and in this case saves us some bit of writing. And always having an ID column is a good DB practice anyway.

Now we run the migration to create our paintings table in the database.

minion db:migrate

We got the table now we can create the model corresponding to the table. Put this file in your APPPATH/classes/model/painting.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php defined('SYSPATH') OR die('No direct script access.');

class Model_Painting extends Jam_Model {
  
  public static function initialize(Jam_Meta $meta)
  {
      $meta->fields(array(
          'id' => Jam::field('primary'),
          'name' => Jam::field('string'),
          'file'=> Jam::field('upload', array('server' => 'local')),
      ));

      $this->validator('file', 'name', array('present' => TRUE));
  }
}
?>

Here we add the 3 columns from the table to map them to model fields. Primary field is a special one, used for fining by index later on. As for the ‘file’ field - it’s an “upload” field which requires you to set a server for the upload. You will generally keep your files on your local machine - and to do that we set the server to ‘local’, but you can set this to various other server types, for example rackspace cloudfiles or ftp server - and it will upload the files there and display them with the appropriate public URL - but don’t worry about that for now.

The default upload locations for local are DOCROOT/upload/temp, and DOCROOT/upload/{model}/{id}/ but you can change them in the config. And we also add validation to require the presense of both name and file fields so the model will not be valid if the user does not input any of those two.

So anyway, we now proceed to the controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php defined('SYSPATH') OR die('No direct script access.');

class Controller_Paintings extends Controller
{
  public function action_show()
  {
      $painting = Jam::factory('painting', $this->request->param('id'));
      $this->response->body(View::factory('paintings/show', array('painting' => $painting)));
  }

  public function action_new()
  {
      $painting = Jam::factory('painting');

      if ($this->request->method() === Request::POST)
      {
          $data = $this->request->post();

          if (Upload::not_empty($_FILES['file']))
          {
              $data['file'] = $_FILES['file'];
          }

          if ($painting->set($data)->check())
          {
              $painting->save();
              $this->request->redirect('paintings/show/'.$painting->id());
          }
      }

      $this->response->body(View::factory('paintings/form', array('painting' => $painting)));
  }
}
?>

With the show action we just display the painting, While the secound - “new” action does all the work of uploading, validating and saving the model. The uploading magic happens in “check” method - we upload the file to the temporary folder and serve it from there, should the validation fail - we will still have the file uploaded so the user will not have to upload it a second time, and we can also display a thumbnail. If everything is OK then ->check() returns TRUE, and in the ->save() method the file gets moved to its final destination.

The corresponding views to make this all work are:

APPPATH/views/paintings/form.php

1
2
3
4
5
6
<?php $form = Jam::form($painting) ?>
<?php echo Form::open('paintings/new', array('enctype' => 'multipart/form-data')) ?>
  <?php echo $form->row('input', 'name') ?>
  <?php echo $form->row('file', 'file', array('temp_source' => TRUE)) ?>
  <?php echo Form::submit('submit', "Create Painting") ?>
<?php echo Form::close(); ?>

APPPATH/views/paintings/show.php

1
<img src="<?php echo $painting->file->url() ?>" alt="<?php echo $painting->name() ?>"/>

Well that’s out of the way and we can start palying with this in our browser. But there are still some things we can modify to make this even better. Say you want to limit to uploading to only images, by using the built in uploaded validator, and we’ll add a thumbnail while we’re at it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php defined('SYSPATH') OR die('No direct script access.');

class Model_Painting extends Jam_Model {
  
  public static function initialize(Jam_Meta $meta)
  {
      $meta->fields(array(
          'id' => Jam::field('primary'),
          'name' => Jam::field('string'),
          'file'=> Jam::field('upload', array(
              'server' => 'local'
              'thumbnails' => array(
                  'thumb' => array(
                      'resize' => array(150, 150)
                  )
              )
          )),
      ));

      $this
          ->validator('file', 'name', array('present' => TRUE))
          ->validator('file', array('uploaded' => array('only' => 'images', 'minimum_width' => 200, 'minimum_height' => 200)))
  }
}
?>

Now we can add a thumbnail to form field, so that when the validation fails, we can see the image that’s already uploaded:

1
2
3
4
5
6
7
8
9
<?php $form = Jam::form($painting) ?>
<?php echo Form::open('paintings/new', array('enctype' => 'multipart/form-data')) ?>
  <?php echo $form->row('input', 'name') ?>
  <?php if ( ! $form->object()->file->is_empty()): ?>
      <img src="<?php echo $form->object()->file->url('thumb') ?>" alt="<?php echo $painting->name() ?>"/>
  <?php endif ?>
  <?php echo $form->row('file', 'file', array('temp_source' => TRUE)) ?>
  <?php echo Form::submit('submit', "Create Painting") ?>
<?php echo Form::close(); ?>

The upload Field is very flexible - it can accept different sources for the images, for example you can pass a URL to the field and it will download it. You can use php://input for ajax uploads, or if you upload it manually to the temp dir by any other method (Flash uploader for example) it will work appropriately.

As a last touch we will add 2 columns to the table - file_width and file_height - and set it up so they get automatically populated with the width and height of the image.

minion db:generate --name=add_file_width_and_file_height_to_paintings
minion db:migrate

Jam Fields only know and can operate on themselves, and can’t change other fields. So this must be done through a behavior. Luckily there is just such a bihavior.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php defined('SYSPATH') OR die('No direct script access.');

class Model_Painting extends Jam_Model {
  
  public static function initialize(Jam_Meta $meta)
  {
      $meta->behaviors(array(
          'file' => Jam::behavior('uploadable', array(
              'save_size' => TRUE,
              'server' => 'local'
              'thumbnails' => array(
                  'thumb' => array(
                      'resize' => array(150, 150)
                  )
              )
          ))
      ));

      $meta->fields(array(
          'id' => Jam::field('primary'),
          'name' => Jam::field('string'),
      ));

      $this
          ->validator('file', 'name', array('present' => TRUE))
          ->validator('file', array('uploaded' => array('only' => 'images', 'minimum_width' => 200, 'minimum_height' => 200)))
  }
}
?>

And we’re done. Everytime a new image is uploaded we will get a proper file_width and file_height.

The Joy of (Func)Testing

Testing in PHP has always been hard - PHP Devs generally just don’t “get it”. There have been some recent developments from Symfony camp, but again they are not easily integratable with Kohana - the framework I’m most interested in. And the saddest thing is there are great testing frameworks out there in other languages and the fact that nobody has tried to accomplish something similar in PHP is just heartbreaking. I mean - the best answer to “how to do functional testing in PHP” is “selenium with PHPUnit”, even if what you want to test is just some form input without any javascript - that can get tough on you.

Anyway I rolled up my sleeves and started on the long and perilous journey of writing my own testing framework. Well not exactly - I mostly got the DSL idea from Capybara, and wrote it on top of Kohana Unittest (PHPUnit) but the result is I think quite significant in itself.

FuncTest

The core principles of FuncTest are to be as easy to write the test as possible, and to execute it as fast as possible. Great! But what does that mean in practice? Here’s an example test, straight from the docs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php defined('SYSPATH') OR die('No direct script access.');

class myTest extends FuncTest_TestCase {

  public function test_finders()
  {
      $this
          ->visit('/my/url')
          ->fill_in('Name', 'John')
          ->fill_in('Username', 'john-user')
          ->select('Select Year', '2004')
          ->check('Recieve Newsletter')
          ->click_button('Create User');
  }
}
?>

Looks easy right? Well it will do exactly what you think it would:

  • Go to URL /my/url
  • Fill in the input tag labeled with ‘John’ (yes labeled - like a label tag with a for=’id’ that matches the id of the input tag)
  • Fill in the input tag labeled Username with ‘john-user’
  • Select 2004 from the dropdown labeled ‘Select Year’
  • Check the checkbox labeled ‘Recieve Newsletter’
  • Click the button ‘Create User’

Why use titles / labels instead of ‘name’ attributes or even css selectors, well - if you wanted to you can, and that too is pretty easy, but in my experience the texts change much less frequently than the internal stuff.

And the best part is - this will work with the Kohana classes themselves internally (actually using HMVC patterns for special internal requests) - so there are no network, apache or Kohana framework overhead whatsoever.

And … just one more thing - if you want to run this in selenium (for ajax testing for example): just add public $driver_name = 'selenium' to your class - and you’re good to go - the same test will run by actually opening up your browser and filling in these fields.

Managing Unreliability With Services

Today I’m gonna introduce the services manager module. It’s probably the project that is most unfinished and a little bit rough around the edges, however even in its current, toddler state it can seriously reduce the complexity of relying on 3rd party services. What you can do with it is encapsulate all the code needed to talking with a 3rd party service - be it Google Analytics, Facebook SDK or even Beanstalkd queue in a single class / library. But not any old class - it’s built around the idea that if a service goes “oops” and doesn’t work anymore for whatever reason - you can disable it and still have a working application. Having such a separation is also great if you want to have different services working in Testing / Development / Production (or even working with different configs). Additionally each service handles its own javascript / css inclusions so this happens transparently to you. You can even say “I want to run this service in development, but on production I want only normal users to see it, exclude the admins”.

You might tell yourself “why do I need this, including a bunch of Google Analytics code is not that hard” - and you would be right - but the more services you rely on the harder it gets to manage them (like - exponentially harder), so why not make your life easier from the beginning.

So, lets try a concrete example - we want to add Google Analytics. Our config code would look something like this:

1
2
3
4
5
6
7
8
9
10
11
<?php defined('SYSPATH') OR die('No direct access allowed.');
return array(
  'services' => array(
      'googleanalytics' => array(
          'api-key' => '{api key}',
          'enabled' => Kohana::$environment === Kohana::PRODUCTION,
          'disabled-for-role' => 'admin',
      ),
  ),
);
?>

And you will need to have a <?php echo Service::all_bodies(); ?> in the end of your main HTML template body, and <?php echo Service::all_heads(); ?> in the head tag of the template, allowing any service to position its asset files in the most convenient location. But that’s about it - you now have a service that works only in Production environment, and even then it will be disabled for logged in users that have the role “admin” - pretty neat, ah?

There is a lot of built in services already available, for example, enabling chartbeat is just a few lines of config:

'chartbeat' => array(
    'enabled' => Kohana::$environment === array(Kohana::PRODUCTION,
    'config' => array('uid' => 'your key' , 'domain' => 'your domain'),
),

And you don’t have to worry where the javascript will go, and not tracking people in dev environment, and generally makes you a more happy developer.

But the main idea is that any service is easily converted to this system and you can create your own services for internal APIs and whatnot with the same flexibility of deployment. You just have to override the relevant abstract methods and you’re good to go.

New Open Source Libraries

I’ve always been a believer that in order to write first class code, one has to write it as if they will be released in the wild - with all the consistency, documentation and general bad-ass-ness that it requires. And that’s the test I generally use to find out if I’ve written a mess - “Can I release this in the wild with my name attached to it”. Well if the answer is “yes” and I will not have the urge to hide in a dark corner whenever I see someone else using it, then what the hell, why not actually release it.

So to put my money where my mouth is, here are the libraries that I’ve decided to release open source. They are all Kohana 3.2 modules for now, with the ambition to upgrade them to 3.3 in the near future.

  • Jam - ORM replacement module, inspired by Kohana Jelly
  • Jam Auth - user authentication and permissions with Jam, as well as external service login support (e.g. facebook)
  • Services Manager - A library to encapsulate talking to external services, with different configs for different environments
  • FuncTest - Functional Testing, inspired by ruby’s Capybara, with selenium and native drivers
  • Jamaker - A Database population tool, inspired by Factory Girl

These are all great tools that we use daily in production and have a lot of documentation and examples in their respective README’s, I’ll be really happy to get some feedback on these.

Also, some of the previously released libraries have been updated

I’ll try to present each one of these libraries on its own in future posts, but for now you can check out the docs of the libraries themselves.

Jamming to the Future

It’s been a long time since my last publication, but I’ve not been gone to a deserted island or anything, just working quietly in the shadows. And I think I am at last ready to come out and introduce what I and the Clippings team have been working on for so long. But first a very quick history tour.

The Kohana framework is really great most of the time - great request handling, caching, module management, database query building etc., where I find it somewhat lacking (and I mean like What-the-hell-guys-put-your-shit-together type) - is the ORM. Yes it works for most of the basic use cases but its only a very thin layer over the Database builder. By the time we started the project there was Jelly - a really inspired little ORM that promised a lot and did it with very little clean and concise code. I immoderately fell in love and started using it instead the built in ORM - and it was working great, up until the point I realized that it was a “dead” project and nobody actually added new stuff to it. No problem - I like to extend things - so little by little small pieces of code were added to the project by us and at one point I just decided - this is not Jelly anymore! And so we now have a new feature rich library for handling database persistence - Jam.

While the Jam library is a spiritual successor of Jelly, the internals are quite different because of the loads of bundled up functionality that had to be added. Because it was developed over time over the Jelly code base, some of its design don’t fit particularly well and will probably be replaced by a more consistent interface in the near future. And I had to remove the eager loading functionality (load_with()), which in my tests performed worse than simply loading all the models anyway, however, you gain quite a lot in return - stuff like:

  • Lazy loading of collections - the query is executed at the last possible moment so you can easily cache it in your views.
  • Build in validation in the model, Active record style - no more unhanded exceptions and weird error message file locations, and validation rules are handled much more developer friendly
  • Polymorphism - polymorphic ‘belongs to’ and ‘has many’
  • Uploading files to different back ends transparently (local, Rackspace, FTP), and by different means (Ajax uploads, URLs)
  • Built in behaviors - Paranoid, Sluggable, Nested, Sortable, Uploadable
  • HTML Form builder with automatic errors integration and nested associations

And these are just off the top of my head - there are many many small additions and fixes, and most importantly tons of documentation for the existing functionality.

So take a look at Jam (and the corresponding Jam Auth) and tell me what you think.


P.S. Kudos to Jonathan Geiger and all of the maintainers of the jelly project - it really is a great inspiration. I tried to hold the spirit of jelly true, but with all the new functionality that had to be added I sort of abandoned the idea of “keeping it really small” and went with “add a lot of useful features” approach.

Writing Custom Widgets for Form Builder

Normal Widget

In the last post about the form builder for Kohana 3.2 I’ve showed some basic examples of using the library. It’s great for when you quickly want to build a form and be done with it, but what if you have something more specific in mind - for example, a widget to display a rich textarea. (using CKEditor)

So here we go - in order to write a custom widget we first need to create a class to hold the widget itself (all widgets are just static methods in some a class). The class must start with Form_Widgets_. So we create one named Form_Widgets_Custom

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Form_Widgets_Custom
{
  static public function richtextarea(Form_Widget $data)
  {
      $data->attributes(array('data-type' => 'rich'));
      $data->attributes()->merge_as_data($data->options("config"));

      return Form::textarea($data->name(), $data->value(), $data->attributes()->as_array());
  }
}
?>

That’s a pretty simple widget from PHP’s perspective - we assign a custom attribute (to anchor our JS). Also - $data->attribute() is not just a simple associative array - it has some helper methods, and one of them is ->merge_as_data() it will perform the same thing as the code above it - merge the array to itself, except it will prefix every key with “data-” so they become proper HTML5 attributes. So the JS, using jQuery will end up looking like this:

1
2
3
$('textarea[data-type=rich]').each(function(){
  $(this).ckeditor($.extend({width: $(this).outerWidth(), height: $(this).outerHeight() }, $(this).data()));
});

You can call this custom widget in any form from now on like this:

1
<?php echo $form->row('custom::richtextarea', 'description') ?>

Multiple Fields Widget

I’ve had to implement a “location” widget, that would allow inputing an address field, showing you the actual location of the address in Google maps, and the ability to move the marker and set a different location for the same address, not the one provided by Google maps. This requires at least one more field, probably two (latitude and longitude).

Each widget takes a Form_Widget class as an argument which has all the information required to build a widget (name, value, attributes, etc.) But that’s for only one field, what if we want some more fields in the same widget - as it happens you can do that:

1
<?php echo $form->row('custom::location', array('location', 'lat' => 'lat', 'lon' => 'lon')) ?>

$data->name(), $data->value() return the name and value of the first field respectfully - but if you have multiple fields, you can get them with $data->items('lat')->name(). So this will work like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class Form_Widgets_Custom
{
  static public function location(Form_Widget $data)
  {
      $data->attributes( array('data-location' => 'location'));
      
      $html = Form::input($data->name(), $data->value(), $data->attributes()->as_array());

      if( $data->items('lat') AND $data->items('lon') )
      {
          $html .= Form::hidden($data->items('lat')->name(), $data->items('lat')->value(), array("data-type" => 'location-lat'));
          $html .= Form::hidden($data->items('lon')->name(), $data->items('lon')->value(), array("data-type" => 'location-lon'));
      }
      return $html;
  }    
}
?>

We set the attributes of the main input field with the $data->attribute(). There are 2 main groups of methods on the Form_Widget passed to our method:

  1. Field Related - those are ->name(), ->value(), ->field_name(), ->label(), ->id() and ->errors(). All of those return values related to first field, but if there are multiple fields, you can access them through the ->items() and ->item(<name>) methods - each item has all those methods too.
  2. Everything else - ->attributes(), ->options(), ->required() - those operate on the widget itself.

Widget Slots

If you want to get really funky with your widget you can tap into the “slots” functionality - you see, the return of the widget method is passed into the :field slot inside the template, but you can modify that directly, set other slots, or even not set a :field slot at all

A simple example of how this all works is this:

1
2
3
4
5
6
7
8
9
<?php
class Form_Widgets_Custom
{
  static public function checkbox(Form_Widget $data)
  {        
      return Form_Widgets::checkbox($data->swap_slots(":label", ":field"));
  }
}
?>

With this we use the default checkbox widget, but swap the slots of the field and label in the template (so that the input appears before the label). You can modify the template directly through $data->template(<new template>) and you can set slots, default ones or your own with $data->slots('name', 'slot content')

Custom Form Builder

Alright so you’ve made a couple of custom widgets, but what you want is a custom style to all your widgets, in that case you can write a custom form builder class that sets the desired parameters to all the widgets it creates. Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
class Form_Builder_Jelly_Admin extends Form_Builder_Jelly
{
  public function row($callback, $name, $options = null, $attributes = null )
  {
      $help = Arr::get($options, 'help');

      $field = $this->field($callback, $name, $options, $attributes);

      $errors = join(', ', Arr::flatten((array) $field->errors()));

      return $field
          ->slots(":errors", "<span class=\"help-inline\">{$errors}</span>", TRUE)
          ->slots(":with-errors", $field->errors() ? 'error' : '', TRUE)
          ->slots(":help", $help ? "<span class=\"help-block\">$help</span>" : '', TRUE)
          ->render();
  }

  //Set some custom parameters in the widget object, used by "row" and "field" methods
  public function widget($name)
  {
      return parent::widget($name)
          ->template('<div class="clearfix :name-field :type-field :with-errors">:label<div class="input">:field:errors:help</div></div>');
  }
}
?>

So now you have a custom “help” option that every widget has, as well as custom error message and a custom template.

Introducing Kohana Form Builder

I have a history with forms - all kinds of them - Symfony Forms, Cake Forms, Kohana Forms, Rails Forms - you name it. And every form builder had something missing - some feature that I had to build, some misconception about what is really desired of the builder. Some got it better than others - for example I am a big fan of rails form helper thingie. It has a ease-of-use feel to it that I hadn’t encountered elsewhere - but it does have some missing features that I always ended up implementing myself. Symfony on the other hand has quite a lot of features but it feels so complicated and cumbersome to use that I often don’t even bother. So I needed a form builder for Kohana 3.2 and decided to build one that had everything I dreamed of having - of course I ended up with a lot of compromises - but the result so far looks quite satisfying - form-builder

Requirements

I’ve put a lot of effort to make writing forms as effortless as possible. There are some basic requirements that I want out of the form builder module:

  1. Configurability - every project’s forms have different requirements of “when to show what” - this had to be implemented in such a way that you would be able to go straight down to HTML and basic PHP to customize your forms, preferably in a repeatable way.
  2. Jelly integration - you would probably have a lot of logic for validation and fields themselves already - I wanted to tap into this instead of duplicating it elsewhere.
  3. Stand on their own - but sometimes you do want to build forms without a specific jelly model attached to it, maybe with just a Validation object, or no object at all
  4. Nested forms - you might say that this is too specific and rarely used - but in every single project I dealt with there had to be at least one nested form - and it’s such a basic concept that I did want it to work right out of the box
  5. Extensibility - you must be able to write new widgets easily - with different properties - multiple fields, with required or optional parameters and to have access to the whole model
  6. Ease of writing - writing HTML forms in your templates should be easy and concise - period.

I’ve tried to address all of those properly and if I’ve strayed off this path I would be happy to receive suggestions how to get back on track :)

Example

So here’s an actual example of a form:

The Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class Controller_Users extends Controller_Template
{
  public function action_edit()
  {
      //Load 
      $user = Jelly::factory('user', $this->request->param('id'));
      $form = Form_Builder::factory($user, $_POST);

      if($this->request->method() == Request::POST AND $form->check())
      {
          $form->save();
          $this->redirect("/success");
      }
      $this->template->content = View::factory('form', array('form' => $form));
  }
}
?>

And the view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php echo Form::open('/edit') ?>
  <fieldset>
  <legend>Basic Info</legend>
      <?php echo $form->row('input', 'email', null, array('type' => 'email')) ?>
      <?php echo $form->row('input', 'first_name') ?>
      <?php echo $form->row('input', 'last_name') ?>
      <?php echo $form->row('input', 'password') ?>
      <?php echo $form->row('checkboxes', 'roles', array('choices' => Jelly::query('role')->select())) ?>
  </legend>

  <fieldset>
  <legend>About</legend>
      <?php echo $form->row('checkbox', 'receive_newsletter', null, array('class' => 'span8')) ?>
      <?php echo $form->row('textarea', 'description', null, array('class' => 'span12')) ?>
  </fieldset>

  <fieldset>
  <legend>Contacts</legend>
      <?php echo $form->row('input', 'website') ?>
      <?php echo $form->row('input', 'twitter') ?>
      <?php echo $form->row('input', 'facebook') ?>
  </fieldset>

That form is sufficient to view and edit the user’s properties, displaying errors on fail when appropriate and it’s all quite self-explanatory - the only concept different from the already established ones in Kohana and Jelly is the concept of “widgets” - you basically have functions that construct some kind of input tags - in this example they are input, textarea, checkbox and checkboxes. All of this is explained in the README

The icing on the cake is that it reads Jelly Model’s rules and places html5 form validation whenever possible.

Big Update to Migration

I was talking to the guys in the Kohana forum and the subject of “why should I use timestamped-migrations instead of minion’s migrations” came up. Why indeed. So I decided to up the game a bit. Doing funky stuff like reading the changes from the local database and then syncing those back to production is a bit too fragile for my tastes, and at least with RDBMS i want to be really sure what’s going to happen and what SQL queries will run, so I’m trying to reduce the “magic” as much as possible. There is however a lot of room for improvement, so what I ended up implementing was rails-style parsing of the filename to generate.

Migration Name Parsing

So imagine you want to make a migration like this:

1
php kohana db:generate add_created_at_and_title_to_users

Now, the tool should be savvy enough to read this filename and at least guess what you want to accomplish. And now it is. Behold:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Add_Created_At_And_Title_To_Users extends Migration
{
  public function up()
  {
      $this->add_column('users', 'created_at', 'string[255]');
      $this->add_column('users', 'title', 'string[255]');
  }
  
  public function down()
  {
      $this->remove_column('users', 'title');
      $this->remove_column('users', 'created_at');
  }
}
?>

It’s not perfect, and you still have to tweak it a bit (change the type of created_at to a timestamp maybe) but it’s a whole lot easier than having a blank migration file.

But where this functionality really shines is when you want to drop some tables / columns.

1
php kohana db:generate drop_table_roles

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class Drop_Table_Roles extends Migration
{
  public function up()
  {
      $this->drop_table('roles');
  }
  
  public function down()
  {
      $this->create_table('roles', array(
          'name' => array( 'string[32]', "null" => false),
          'description' => array( 'string[255]', "null" => false)
      ), array( 'engine' => 'InnoDB' ));
  }
}
?>

You no longer have to go through the current database to write your own down statement, this data is accessible to the tool, so it does the work for you. Of course if the table does not exist it behaves like normal and you still have to write your own down statement.

So Available patterns right now are:

  • create_table_{table}
  • drop_table_{table}
  • add_{columns}_to_{table}
  • remove_{columns}_from_{table}
  • change_{columns}_in_{table}
  • rename_table_{old_name}_to_{new_name}
  • rename_{old_name}_to_{new_name}_in_{table_name}

And you can write several {columns} if you separate them with _and_ and you can also write several patterns, separated with _also_ :

1
php kohana db:generate drop_table_roles_also_add_name_and_title_to_authors

It will order the statements in the up and down methods of the migration correctly too.

Dry Runs

There’s now the ability to run the migrations and see what they do, without actually doing anything. Just add a –dry-run:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
php kohana db:migrate --dry-run
1321124936 Add_Created_At_And_Title_To_Users : migrating up -- Dry Run
-- [dry-run]add_column( users, created_at )
   --> 0.0000s
-- [dry-run]add_column( users, title )
   --> 0.0000s
1321124936 Add_Created_At_And_Title_To_Users : migrated (0.0001s)
1321124960 Drop_Table_Roles_Also_Add_Name_And_Title_To_Authors : migrating up -- Dry Run
-- [dry-run]drop_table( roles )
   --> 0.0000s
-- [dry-run]add_column( authors, name )
   --> 0.0000s
-- [dry-run]add_column( authors, title )
   --> 0.0000s
1321124960 Drop_Table_Roles_Also_Add_Name_And_Title_To_Authors : migrated (0.0001s)

So no worries.

Execute SQL

After adding the dry run ability the need for a dedicated method to execute arbitrary SQL arose - so it will not execute on the dry run. You can now do:

1
$this->execute( "INSERT INTO `roles` (`id`, `name`, `description`) VALUES(1, 'login', 'Login privileges, granted after account confirmation')");

And when you make a dry run this will not execute.

Bug Fixes

A nasty bug was fixed when when you specified already executed migration with –version it would execute all the others. Anyway, you should sleep happy now.

Check out the code: timestamped-migrations.

Timestamped Migrations

All non-trivial web projects that use RDBMS need some sort of database migration system to manage database changes and schema evolution. But the tools for that for Kohana 3 are of course … you got it - lacking. There is Kohana-3-migraitons - but versioned migrations work only for a single developer - if that’s your situation - I envy you, I really do - so much less headache. But when you try to collaborate database changes, especially with git/svn managing your files - you soon start to say very inappropriate words, very very loudly. So I went on and rewrote null77’s excellent module kohana-migrations for Kohana 3 - Timestamped Migrations, but with some more sugar.

1
git clone git://github.com/OpenBuildings/timestamped-migrations.git modules/timestamped-migrations

Or

1
2
git submodule git://github.com/OpenBuildings/timestamped-migrations.git modules/timestamped-migrations
git submodule update --init

So now your migrations will look like

1
2
1319717756_initial.php
1319717856_migrate_users_and_roles.php

Might seem a bit too verbose, but it’s a godsend when you have more than one person on a project - you can just drop a migration file and have it execute in just the right place and order - delightfully convenient.

I personally detest web interfaces for those kind of tasks - exposing system tools to a webserver seem … unclean, so I’ve ditched the controllers and views, and implemented simple CLI for this, using Kohana-cli and mimicking rails’ syntax as much as possible. Check it out.

1
2
3
4
5
6
kohana db:version
kohana db:migrate
kohana db:migrate:up
kohana db:migrate:down --step=4
kohana db:migrate:redo --version=1319717756
kohana db:rollback

You can read all about it at the github README file. Enjoy.

Command Line Interfaces Rule

All the proper web framework those days have some sort of Command line tools - the first that I’ve personally encountered where rails and symfony and I haven’t been able to live without them ever since, but Kohana 3 seems to be awfully backwards in that respect - no CLI in 2.3 and now no CLI in Kohana 3. They did structure it very well to create those tools - its just the tools themselves seem to be missing. Anyway here’s my take on this.

Kohana-cli

1
git clone git://github.com/OpenBuildings/kohana-cli.git modules/kohana-cli

Or

1
2
git submodule git://github.com/OpenBuildings/kohana-cli.git modules/kohana-cli
git submodule update --init

To use it properly after you’ve downloaded the module you have to symlink or copy the kohana file from the root module directory to your root directory.

1
2
3
4
cp modules/kohana-cli/kohana ./

php kohana help
./kohana help

Or if you want system-wide support

1
2
3
cp modules/kohana-cli/kohana /usr/local/bin/

kohana help

Alright - so now you’re set to explore what commands you have

1
kohana list

This will go through all your modules and search for specially designated “command” folders which contain cli scripts. Now several of my own modules have predefined command line utilities but kohana-cli comes with some useful ones itself.

1
2
kohana cache:clear
kohana module:generate

You can get help for the commands with the usual syntax

1
2
kohana help cache:clear
kohana help module:generate

And it’s ridiculously easy to write your own CLI commands with color output and all that check out cache:clear’s source:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Command_Cache extends Command
{
  const CLEAR_BRIEF = "Clear system cache and Cache";
  public function clear()
  {
      //Call function and profile its start and end, last argument is the color of the output.
      self::log_func(array(Cache::instance(), 'delete_all'), null, Command::OK);
      self::log_func("system", array("rm -rf ".Kohana::$cache_dir."/*"), Command::OK);
  }
}
?>