Want to get Lullabot article, videocast, and podcast announcements delivered right to your in-box? Let us know your email address (we won't share it) and we'll let you know when anything exciting happens.

Moving CCK Field Changes from Dev to Live

This article applies to Drupal 5 and higher.

If you're the maintainer of a even a small site using CCK for managing your information, eventually you'll run into a problem synchronzing your local install with your live site (or Dev and QA servers). While setting up CCK fields is a breeze and you have SVN to manage your PHP code, many Drupal developers haven't found a way to easily migrate changes in the CCK data schema or content type fields. However, using the handy macro module and a few (very important) lines of custom code, you can be moving your field changes from Dev to live in seconds.

To clarify the task at hand, say you have a content type called "article". The article type contains 3 fields at the moment: Title, Body, and Picture. The problem is, your site is about to undergo a major overhaul where articles now contain Title, Excerpt, Body, Picture, Caption, Small Picture, and Related Links. Additionally, your boss wants them seperated into different field groups to make it easier to handle the more complex form.

Here's what we want our article content type fields to look like, before and after.

Before overhaul: Before content type Our updated content type: After content type

This article assumes you've already made your changes to your PHP code and have them ready to deploy through SVN, but you don't want to have to setup the changes to the content type manually on the dev server, qa server, live server, and potentially several of your coworkers' computers manually.

drupal_execute and Macro module

Fortunately, Drupal has a convient mechanism for being able to execute any form programatically. It's called drupal_execute(). Though often over-hyped and under-utlized, drupal_execute() can save us tremendous amounts of work. Drupal execute takes two arguments, the name of the form to execute, and an array of values to pass into the form.

Continuing down the list of cool things Drupal gives you, if you've downloaded and installed Devel module, it comes with a bundled module called Macro. Macro has the ability to record all your form submissions, then play them all back on different servers.

Enabling Macro module

Now with Macro module enabled, we need to turn on form recording at admin/build/macro.

Turning on Macro recording

Now at this point, Macro module is watching all your form submissions and saving all the values for you. Go to your admin/content/types page and start making all the necessary adjustments to your content types. Add new types, add new fields, change field properties, make groups, arrange them however you want. Macro module is quietly watching you.

A sample macro

When you're done, skip over to admin/build/macro/export. You should have a field waiting for you that contains something like this:

This is an actual export of the above screen shot you can try on your own local install.

Caveats to macro module

Once you've got the contents of the macro saved to a text file (just copy and paste), head over to admin/build/macro and turn off the macro recording.

You now have a macro which you should be able to run on your QA and Dev servers. Starting with a fresh copy of Drupal, try running your macro, but pay no attention to the following lines....

  • warning: call_user_func_array() [function.call-user-func-array]: First argument is expected to be a valid callback, 'node_type_form' was given in /Users/nate/Sites/drupal5/includes/form.inc on line 217.
  • An illegal choice has been detected. Please contact the site administrator.
  • An illegal choice has been detected. Please contact the site administrator.
  • The settings have not been saved because of the errors.
  • The configuration options have been saved.
  • There are no groups configured for this content type.
  • An error has occured with macro #3 , form_id content_admin_field_overview_form. Please check the errors displayed for more details.
  • An error has occured with macro #4 , form_id macro_admin_settings. Please check the errors displayed for more details.
Sample errors when trying to run macro module

Oh no! what's this? Errors galore! This example exposes some of the largest short-comings of Macro module. Reading between the lines, the first error is basically saying it couldn't find the form "node_type_form" to execute. Some of the later problems such as "There are no groups configured for this content type." signal a much different problem.

Solutions to macro shortcomings

To fix the first one, we just have to make sure that the node_type_form is loaded. As an optimization, Drupal only loads this form if you're actually on the page that uses it. For a quick job, I'd just open up Macro module and add the necessary files.

<?php
/**
* Implementation of macro_import_macro hook_submit function.
*
* Plays back the submitted macro.
*/
function macro_import_macro_submit($form_id, $form_values) {
  include_once
'./includes/install.inc';
  include_once
'./modules/node/content_types.inc'; // Add this line!
 
eval($form_values['macro']);
 
drupal_execute_macro($macro);
}
?>

The second problem that exists with Macro module is it's inability to give each form a clean start. Because many forms assume that they are the only thing running in a single request, many of them use caching mechanisms that will cause later form executions to fail. These include static variables in several core functions, such as node_get_types(). If you make a new type early in your macro, the new type won't be available to subsequent form executions. To fix this, we can clear these caches between each form submission.

Once again, open up macro module and change these lines:

<?php
/**
  * Attempts to programmatically submit all the forms that have been specified in the $macros collection.
  */
function drupal_execute_macro($macro) {
  foreach (
$macro as $key => $data) {
   
// print_r($data);
   
$param = unserialize($data['parameters']);
   
$args = array($data['form_id'], $data['values']);
   
$args = array_merge($args, $param);
   
call_user_func_array('drupal_execute', $args);

   
// Clear static variable caches
   
fieldgroup_groups(NULL, NULL, TRUE); // fieldgroup.module
   
_content_type_info(TRUE);            // content.module
   
node_get_types(NULL, NULL, TRUE);    // node.module

   
if (form_get_errors()) {
      
drupal_set_message(t("An error has occured with macro #%macro_number , form_id %form_id. Please check the errors displayed for more details.", array('%macro_number' => $key, '%form_id' => $data['form_id'])));
    }
  }
}
?>

Now run your macro again. Ta-da! A complete update of all your new content types, fields, groups and properties. If you use Macro module for other work, you'll want to roll back your changes as they can cause a lot of unnecessary changes for other macros that might be run. Macro module could also probably be improved to make such hacking unnecessary via a custom module, but for the time being, each macro will require it's own set of caches cleared and PHP files included manually.

Comments

Selenium

You can also do the same thing with the excellent FireFox extension Selenium:

http://www.openqa.org/selenium

Nice. Server vs. client

This seems like a decision between using client-side technology versus server-side. As long as the solution is portable, either one would work. I've never used selenium, but I imagine that the server-side approach would have a speed advantage over something that needs to make HTTP requests. Though which ever works, I'd stick with that :)

content_copy.module ?

Can't the CCK supplied content_copy.module not be used to do the same CCK type/field import/export operations.

I'm not sure, but doesn't

I'm not sure, but doesn't the content_copy.module only allow you to copy a completely new content type and not modify an existing one?

Nope, you can modify too!

For instance, to add an Image field to the Story content type you could use

<?php
  $modulepath
= drupal_get_path ('module', 'my_module');
 
$cck_definition_file = $modulepath."/story_type.cck";

 
$values['type_name'] = 'story';
 
$values['macro'] = file_get_contents($cck_definition_file);

  include_once(
drupal_get_path('module', 'node') .'/content_types.inc');
  include_once(
drupal_get_path('module', 'content') .'/content_admin.inc');

 
drupal_execute("content_copy_import_form", $values);
?>

The important thing to note is the line:

<?php
   $values
['type_name'] = 'story';
?>

And that we use 'story' rather than 'Story' i.e. the actual type name rather than the human friendly one.

The file story_type.cck contains the macro produced after manually adding an Image field and pressing export in the Drupal admin->Content types section:

<?php
$content
[type]  = array (
 
'name' => 'Story',
 
'type' => 'story',
 
'description' => 'Stories are articles in their simplest form: they have a title, a teaser and a body, but can be extended by other modules. The teaser is part of the body too. Stories may be used as a personal blog or for news articles.',
 
'title_label' => 'Title',
 
'body_label' => 'Body',
 
'min_word_count' => '0',
 
'help' => '',
 
'node_options' =>
  array (
   
'status' => true,
   
'promote' => true,
   
'sticky' => false,
   
'revision' => false,
  ),
 
'comment' => 2,
 
'upload' => 1,
 
'nodewords' => 1,
 
'old_type' => 'story',
 
'orig_type' => 'story',
 
'module' => 'node',
 
'custom' => '1',
 
'modified' => '1',
 
'locked' => '0',
);
$content[fields]  = array (
 
0 =>
  array (
   
'widget_type' => 'image',
   
'label' => 'Content Image',
   
'weight' => '0',
   
'max_resolution' => 0,
   
'image_path' => '',
   
'custom_alt' => 0,
   
'custom_title' => 0,
   
'description' => '',
   
'group' => false,
   
'required' => '0',
   
'multiple' => '0',
   
'field_name' => 'field_content_image',
   
'field_type' => 'image',
   
'module' => 'imagefield',
  ),
);
?>

this may be done easier using your database

If you want to port the table structure then the easier way I think would be to dump the scheme for both databases, figure out the differences and then run a single alter table to supply the missing fields with their defaults and any indices just after making a backup of your live database.

After all, that's the end result from making modifications like this, the table structure gets changed.

If you're aware of this ahead of time you could keep track of your alter tables as they occur on the test database to make sure you have them 'just so'.

Make sure you don't forget to make the structure of your test and live database equal before you start working on your test database, that's a fairly easy trap to fall in to and it can lead to hours of fun at the moment of going 'live'.

When making changes like this it's a good idea to do a 'dry run' where you stage to a third subdomain (say, dev for development, stage for testing before going live and www/nohting for live deplyment) to see if everything comes out as expected. This may save you some embarrassment in case things don't work out as planned.

best regards,

Jacques Mattheij

since you have to turn the

since you have to turn the macro recording on before you make your cck changes, doesn't that mean you have to already know what your cck changes are going to be before you make them? so, what do you do, make your cck changes for "practice", then take a screenshot, revert the changes to match then production server, then record the macro while re-creating the form? i guess i don't see how this is easier than just recreating the changes directly on the production server, since you have to do that step either way. is there something i'm missing?

i've been considering drupal for a client project and this issue (the fact that production and dev are for all practical purposes difficult or impossible to synchronize) is probably a deal-breaker. unless i'm misunderstanding, it will be a huge hassle any time the client calls and asks us to fix bugs, develop new features, etc.

Deployment speed

This assumes that you're doing initial development on a sandbox (not live) site initially, where you do the recording. If you're doing changes that require hours of time, this method lets you deploy the changes to QA and Live in a matter of seconds. The configuration absolutely must be done at least once, so this prevents you from having to do them repeatedly for each environment on which you're deploying the changes.

If you're not concerned about down-time on your live site (say during initial site launch). Then you can simply configure directly on the live server then just do database dumps to sync up all your local sandboxes with the live site. This article is for the reverse, where you want to eliminate downtime as much as possible.

another solution for the first error.

Another way to fix this error :
/*
warning: call_user_func_array() [function.call-user-func-array]: First argument is expected to be a valid callback, 'node_type_form' was given in /Users/nate/Sites/drupal5/includes/form.inc on line 217.
*/
By give a macro module a great weight to execute later.

Use the Crud field

Actually with the following patch http://drupal.org/node/254508 in which the crud file is has been backported from Drupal 6 to Drupal 5 it gives Drupal 5 a proper API.

So putting the changes from on site to another via the update.php is quite easy.

<?php
function xxx_update_123() {
  include_once
drupal_get_module('module', 'content') .'/content_crud.inc';
 
content_field_instance_update(array('field_name' => 'field_name', 'weight' => -2));
}
?>

And that is it.

Dbscripts project is a new way to handle this

Kathleen Murtaugh's http://drupal.org/project/dbscripts will automatically push changes -- creations, deletions, and modifications -- of CCK fields to your staging site.

problems with custom types and crud.inc

Im quite new to drupal so im not familiar with core stuff that much. I was really hoping to have this bit working as we will need to migrate data along with code changes between dev/staging/live.

I recorded macro where i create new content_type and add a few fields. I also create some group of rields etc.

I applied all patches in the article but im still getting errors and im not sure is it something easy to fix.

Errors come from crude.inc trying to query fields but passes array instead of string. thats stack

#0 /data/drupal/SSCdrupal/trunk/includes/database.inc(213): db_escape_string(Array)
#1 [internal function]: _db_query_callback(Array)
#2 /data/drupal/SSCdrupal/trunk/includes/database.mysql-common.inc(41): preg_replace_callback('/(%d|%s|%%|%f|%...', '_db_query_callb...', 'SELECT * FROM d...')
#3 /data/drupal/SSCdrupal/trunk/modules/cck/includes/content.crud.inc(473): db_query('SELECT * FROM {...', Array)
#4 /data/drupal/SSCdrupal/trunk/modules/cck/content.module(2437): content_field_instance_read(Array, true)
#5 /data/drupal/SSCdrupal/trunk/modules/cck/includes/content.admin.inc(99): content_inactive_fields(Array)
#6 /data/drupal/SSCdrupal/trunk/modules/cck/includes/content.admin.inc(123): content_inactive_message(Array)
#7 [internal function]: content_field_overview_form(Array, Array, 'macro123')
#8 /data/drupal/SSCdrupal/trunk/includes/form.inc(366): call_user_func_array('c in /data/drupal/SSCdrupal/trunk/includes/database.mysqli.inc on line 358

What is happening in the log is that it seems to call it for all node types (not sure why as i was not adding stuff ther just creating new node type and new fields). Then when it finds my content type that has some custom stuff (i created type before and added some fields so it has a table in the database created) it fails to query it.

Any hints how can i fix it? or could it be caused by some of modules i have installed?

Thanks a million!

Artur

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <blockquote> <h2> <h3>
  • Lines and paragraphs break automatically.
  • Use <!--pagebreak--> to create page breaks.
  • You may post code using <code>...</code> (generic) or <?php ... ?> (highlighted PHP) tags.
  • Web page addresses and e-mail addresses turn into links automatically.

More information about formatting options