My rule of thumb for deciding what to post on this blog has been to document anything I've spent more than an hour trying to figure out. Today I've got a good one for anyone trying to create CCK fields as part of a module's installation process.
Back in Drupal 5 the Station module was made up of lot of custom code to track various values like a playlist's date or program's genre and DJs. During the upgrade to Drupal 6 I migrated that data into locked, CCK fields that were created when the module was installed. As people started to install the 6.x version of module I began getting strange bug reports about the Station Schedule that I couldn't seem to replicate on my machine.
Eventually after trying it on a fresh installation, I discovered the problem was that its fields weren't being created correctly by the hook_install() implementation when CCK and/or the field modules were installed at the same time as the Station modules. Meaning that the user who setup a new Drupal site, downloaded all the modules, checked the Station Schedule check box on the module list and let Drupal figure out the dependencies from the .info files would think the modules had installed correctly but they'd actually be missing several required field instances which would cause errors down the line. My first response to this problem was to add a hook_requirements() implementation that prevented the Schedule from being installed at the same time as the other modules:
<?php
/**
* Implementation of hook_requirements().
*/
function station_schedule_requirements($phase) {
$requirements = array();
$t = get_t();
if ($phase == 'install' && !module_exists('userreference')) {
$requirements['station_schedule_userreference'] = array(
'description' => $t('Sadly the Station Schedule cannot be installed until the User Reference module has been fully installed. User Reference should now be installed, so please try installing Station Schedule again.'),
'severity' => REQUIREMENT_ERROR,
);
}
return $requirements;
}
?>This at least removed the "Surprise, you've got a broken site!" element, but it was annoying to have to reinstall the module. When I realized that it wasn't just the Schedule that was suffering from this problem—but also the Program and Playlist modules—I decided to look for a better solution.
After six hours of debugging via print statement—technically the Devel module's dsm() function (yes, I know the time would have been better spent figuring out how to get a proper PHP debugger running on OS X)—I found it boiled down to two issues:
- The field's columns weren't being populated because the fields'
.modulefiles weren't being included. - CCK uses
drupal_write_record()to record the field information but it was failing becausecontent_schema()wasn't being called.
The first was simple enough to correct, I could manually include the module files. The second was much trickier, drupal_get_schema() calls module_implements() so that it only returns schema information for enabled modules but drupal_install_modules() installs the group of modules then enables the group. I was expecting that when hook_install() was called the required modules would be both installed and enabled. So in order to create my fields in station_schema_install() I'd need to get CCK and the fields enabled first. Feeling close to one of those head slapping moments I started studying module_enable() and realized it seemed safe to call from within a hook_install() implementation. It had the added bonus of including the module which solved the first problem.
I love it when you figure out the right way to do something and it turns out to also be the short way. It's really this simple:
<?php
/**
* Implementation of hook_schema().
*/
function station_schedule_install() {
drupal_install_schema('station_schedule');
// To deal with the possibility that we're being installed at the same time
// as CCK and the field modules we depend on, we need to manually enable the
// the modules to ensure they're available before we create our fields.
module_enable(array('content', 'userreference'));
$dj_field = array (
// FIELD DEFINITION OMITTED.
);
// Create the fields.
module_load_include('inc', 'content', 'includes/content.crud');
content_field_instance_create($dj_field);
}
?>As always, I hope this saves someone else some trouble.
I have come to know about CCK
I have come to know about CCK CRUD via this post. Recently I tried to do something that will make it even easier for developers to deploy changes on a production server, and here it is - http://drupal.org/project/cck_sync
hook_enable()
I've always found it much more reliable to do this type of configuration in hook_enable(). This hook runs on install, but only AFTER all modules scheduled for installation have been installed and loaded. This eliminates the need for the module_enable() call.
An interesting side note: when enabling a bunch of modules at the same time, they are not necessarily installed in the order of dependencies. This is the reason that doing things in hook_enable() is even more important.
I'd considered that
I considered doing it in hook_enable() but decided against it because I wanted to create the fields, some of which are optional, then let the user make changes or possibly remove them. doing it in hook_enable() seemed like it would take quite a bit more work to recreating a field that the user had decided to remove if they disabled and re-enabled the module. it would have been possible but this seemed cleaner to me.
I spent quite a bit of time checking the order of installation and wasn't able to get an incorrect ordering. Looking at the code to drupal_install_modules() puts the most depended modules at the front of the list. I'm having a hard time coming with with a scenario where that wouldn't work... which is not to say it's impossible, just that I'm not smart enough ;)
Thanks for posting this…
I ended up using a variation of this for a client's module. Since the module doesn't work without the field added, I ended up with using hook_enable like this:
<?php
/**
* Implementation of hook_enable().
*/
function dsvp_contest_enable() {
// If our CCK field is not install, install it.
if (is_null(content_fields('field_contest_answers', 'contest'))) {
$field = array(
// Field definition omitted for brevity.
);
// Create the fields.
module_load_include('inc', 'content', 'includes/content.crud');
content_field_instance_create($field);
}
}
dsm()
I only knew of the
dprint_r(), so I had to look updsm():<?php // legacy function that was poorly named. use dpm() instead, since the 'p' maps to 'print_r' ?>Which is, in turn, another alias.
Just a heads up!
Some thoughts
Thanks for the great writeup.
A few thoughts:
* Why not add the other modules as dependencies? That way they have to be installed first (Drupal takes care of this automagically).
* How do you create your field definitions? By extracting the relevant bits from the content type export, or creating it manually based on what the field forms require?
dependencies aren't enough
content, text, etc are actually listed as dependencies. I realize I was a bit ambiguous on this point in the original post, the modules are installed as a group then enabled as a group, so your dependent modules won't actually be available in your hook_install() because they may have been installed but not enabled. I'll update the post to make this bit clearer.
i've got an entire post about programatically creating cck fields.