April 29, 2015
In Drupal 7, deploying new fields from a development environment onto a production server can be a tricky and time consuming business. Imagine, the following scenario:
solutions”.
This process of copying changes across would probably involve putting two browser windows side by side, one on the development site and one on the production site and making sure that all of the field settings match.
Manually recreating fields on a production server is a tedious and error prone business, especially when faced with the prospect of creating multiple new fields. Ideally this process should be automated and completed in as few steps as possible.
Much of the site configuration in Drupal 7 is heavily baked into the site’s database and can be particularly cryptic to unravel. A common way of getting this configuration into code (and therefore under version control and easily deployable) is with the use of the Features and Strongarm modules, but for me this isn’t always a viable option.
The process of using Features and Strongarm to deploy updates is not simple and requires you to split the functionality of your site into self-contained parts of code and configuration, which can be a length and challenging process, especially when working on a site that doesn’t use this method for deployment.
My preferred approach in these situations is to
use a site deployment module. If you’re not familiar with creating a custom
module, take a look through the example modules source code and the
excellent developer guide on drupal.org.
In short, a site deployment module is a custom
module that utilises a .install
file to incrementally update a site. These
updates are run by implementing hook_update_N, where N is the modules
current update number. A basic .install
file for a site deployment module
can be seen on the dcycle site (a fantastic resource for Drupal automation snippets).
If I were to create a new module called “mysite_deploy” containing this code:
/**
* Example update function that disables Overlay
*/
function mysite_deploy_update_7001() {
// some people don't like overlay...
module_disable(array('overlay'));
}
I could then push the module code to my production server, enable this module
through the GUI and then run update.php by clicking on the “update script” link
on the modules page. The function mysite_deploy_update_7001
would then run and
the overlay module would be disabled.
Using this method to create a new field is surprisingly tricky. Firstly you need
to create the new field using field_create_field
, passing
in a valid “field definition array”. The you will need to create an instance for
each place that the new field is used using field_create_instance
, this
time using a “field instance definition array”.
For me, this is a daunting task. The information required to create a new field is as confusing as it is diverse and becomes even more complicated when your trying to deploy a non-standard field such as entity reference. I’m forever forgetting what “widget type” I need to be using and what “cardinality” actually means.
Luckily all this information is available through other Drupal API functions,
if the field has already been created. Using the two functions field_info_field
and field_info_instance
, I can add something like this to my themes
template.php
and get all the information I need (as long as I can figure out what my entity type and bundle is).
$fieldName = 'field_fancy_marketing_text';
$fieldInfo = field_info_field($fieldName);
$bundle = 'product';
$entityType = 'node';
$fieldInstance = field_info_instance($entityType, $fieldName, $bundle);
print_r($fieldInfo);
print_r($fieldInstance);
The problem with this method is that it’s particularly brutal (print_r()
in
template.php?) and I still need to format the printed arrays into valid PHP.
Using var_export
instead of print_r
helps but it all still feels a bit
clunky. It’s not fast and I’m having to edit unrelated files to get the
information I need.
A better solution for me was to use a Drush script. If you haven’t come across Drush before, it is a command line tool that can bootstrap and interface with Drupal installations in various ways, from clearing caches to updating modules. I highly recommend it, it’s become an incredibly valuable part of my Drupal development toolkit.
After creating a field through the GUI on my development site, I can run the
Drush script from the command line, providing the field’s machine name as an
argument. The script pretty prints code that I can then
copy and paste into a hook_update_N
function.
The script uses the provided field name to call field_info_field
and
field_info_instance
, fetching the necessary field definition and instance
definition arrays. These arrays are then beautified and output to the command line
using the heredoc style. This script can be used ‘as is’ as long as it it’s
run from inside a Drupal installation directory.
I’ve posted the complete annotated script as a GitHub gist.
I love this little drush script, but for me it was still bothersome that I had
to drop out of the .install
file I was working on, run the script and copy and
paste the output. So I wrote a Vim plugin!
Using this plugin I can type the machine name of a field, then go to normal mode
and execute the function DrupalExportField
(or map the function call to
a leader key - in the example above I’m using the mapping <Leader>def
). The
word under the cursor is then replaced with the PHP code generated by the drush
script. The code written into the vim buffer can have indentation that doesn’t
match the rest of your file, but by executing the normal mode command =i{
at
the end of the function we can automagically neaten up the spacing inside the
current code block.
If you like, you can copy and paste the code below into your .vimrc file or alternatively you can download the plugin from GitHub here.
" Get the absolute path of this vimscript
let s:path = fnamemodify(resolve(expand('<sfile>:p')), ':h')
function! DrupalExportField()
" Get the word underneath the cursor
let fieldName = expand("<cword>")
" Get the root path of the current Drupal installation
let drupalRoot = system("drush eval 'echo getcwd()'")
" Execute the drush script (the script should be stored relative to this functions absolute path)
let exportCode = system('drush --root=' . drupalRoot . ' ' . s:path . '/fieldexport.drush.php --field=' . fieldName)
" Switch to paste mode
set paste
" Write the export code to the buffer, then neaten up the indentation
execute "normal! mqciw\<F10>" . exportCode . "\<F10>\<esc>'q=i{"
" Exit paste mode
set nopaste
endfunction