Nov 06 2015
Nov 06

The Northwest Atlantic Marine Alliance (NAMA) advocates for healthy marine ecosystems. Through their site, namanet.org, they organize and promote a movement toward a thriving ocean and a just seafood system.

Having helped create the original Drupal 6 site for NAMA in 2008, it’s been a rewarding experience re-architecting it in Drupal 8 (beta). The main purpose of the project, at least initially, was to help NAMA plan for how to deal with their soon-to-be-end-of-lifed D6 site by upgrading it to D7. As a fully matured platform, this upgrade (or migration) path has long been the recommended approach, even since talk of a release candidate began in the summer of ’15, when the number of critical issues neared zero.

Choosing an upgrade path – Drupal 7 vs Drupal 8

NAMA was the perfect client to test out a Drupal 8 upgrade. They were flexible about their design and functional requirements, and the original site was straightforward in its architecture. As the betas ticked up in number, it became feasible to consider skipping over D7 to go straight to D8 instead. Once the D6->D8 migration path appeared ready to use, we discussed the pros and cons with NAMA and, together, decided to go for it.

By structuring the project to partially include “internal” and “personal development” time, we were able to deliver the new site on a tight budget while allowing the entire team to gain crucial experience with the upcoming version of our CMS/framework of choice.

The Drupal 8 advantage

While NAMA will enjoy many secondary benefits to having a D8 site, like a responsive front-end and in-place WYSIWYG editing, the most meaningful advantage of NAMA skipping D7 for D8 is the significantly extended lifespan of their redeveloped site. For Advomatic, apart from getting to work with a shiny new Drupal toy, we’re now prepared to hit the ground running when the full release of D8 is ready, and to support the sea of Drupal 6 sites that will soon be in need of migration help.

NAMA's coordinating director, Niaz Dorry.

NAMA’s coordinating director, Niaz Dorry.

As we wrapped up our final tickets and NAMA began working with their new site, I asked Coordinating Director Niaz Dorry to chat with me about their experience with Drupal 8 so far.

When asked about her favorite features of the new site, Niaz noted that it was, “clean, open, uncluttered, more modern, and (so far) appears to be easier to edit.” She also detailed some of the new configurable components we added to the homepage, which leverage the fieldability of D8’s upgraded Block module.

Niaz added that the move to the new site has been fairly seamless from an administrator’s perspective. The ease of this process is largely due to content creation and usability improvements that have been incorporated into D8 core: 

“The transition seems smooth so far. Generally speaking, the two sites feel similar. What is different is not THAT different, and we seem to be learning it pretty quickly.”

And what about the tradeoffs of working with not-quite-ready software, when a robust ecosystem has been developing around its predecessor for years? At the outset of the project, we’d discussed – in theory – what sacrifices might need to be made. But in practice, what diminished functionality did they end up having to live with?

“Not much, really. Some of the things that are not ready yet – like a better date module – don’t appear to be showstoppers. We knew what we were going into, and that there would be some adjustments to things as Drupal 8 goes from Beta to fully operational. So nothing has been a surprise.”

Niaz eloquently summarizes how this project has been a big win for everyone involved:

“We’re glad to have had this opportunity to work with Advomatic. The results – an updated website for us, experience with Drupal 8 for Advomatic, and contributing to the broader cyber community as we learn – are truly a win-win-win, and that’s the kind of effort we like to engage with, whether it’s our program work, our policy work, the way we run our organization, and now even the way we build our website.”

Have you had a chance to work with a client on a D8 project yet? What do they think so far?

Oct 09 2015
Oct 09

When migrating a site from Drupal 6 to Drupal 8, we had to write some very basic Plugins. Since plugins and some of their related pieces are new to Drupal 8, here is a walk-through of how we put it together:

Use case

In Drupal 6, the contrib Date module provided a date field that had both a start and end date. So, the beginning of Crazy Dan’s Hot Air Balloon Weekend Extravaganza might be July 12, with an end date of July 14. However, the datetime module in Drupal 8 core does not allow for end dates. So, we had to use two distinct date fields on the new site: one for the start date, and one for the end date.

Fields in D6:
1.  field_date: has start and end date

Fields in D8:
1.  field_date_start: holds the start date
2.  field_date_end: holds the end date

Migration overview

A little background information before we move along: migrations use a series of sequential plugins to move your data: builder, source, process, and finally, destination.

Since we are moving data from one field into two, we had to write a custom migration process plugin. Process plugins are where you can manipulate the data that is being migrated.

Writing the process plugin (general structure)

The file system in Drupal 8 is organized very differently than in Drupal 7. Within your custom module, plugins will always go in [yourmoduledir]/src/Plugin. In this case, our migrate process plugin goes in [yourmodulename]/src/Plugin/migrate/process/CustomDate.php.

Here is the entire file, which we’ll break down below.

<?php
/**
 * @file
 * Contains \Drupal\custom_migrate\Plugin\migrate\process\CustomDate.
 */

Standard code comments.

namespace Drupal\custom_migrate\Plugin\migrate\process;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;

Instead of using functions like include(); or include_once(); to add various PHP files, now we “include” them by referencing their namespaces. Or rather Drupal knows which files to autoload based on the namespace.  This way, if a class is ever moved to a new directory, we won’t have to change code elsewhere, as long as the namespace stays the same. We will allow our code to be used the same way, by defining its namespace.

/**
* This plugin converts Drupal 6 Date fields to Drupal 8.
*
* @MigrateProcessPlugin(
*   id = "custom_date"
* )
*/

This class comment includes an annotation. When the Migrate module is looking for all available migration plugins, it scans the file system, looking for annotations like this. By including it, you let the migration module discover your migrate process plugin with the unique id ‘custom_date’.

Our new class will inherit from the ProcessPluginBase class, which is provided by the Migrate module in core. Let’s step back and look at that class. This is it’s definition:

abstract class ProcessPluginBase extends PluginBase implements MigrateProcessInterface { ... }

Since this is an abstract class, it can never be instantiated by itself. So, you never call new ProcessPluginBase(). Instead we create our own class that inherits it, by using the keyword extends:

class CustomDate extends ProcessPluginBase { ... }

The ProcessPluginBase class has two public methods, which will be available in child classes, unless the child class overrides the methods. In our case, we will override transform(), but leave multiple() alone, inheriting the default implementation of that one.

(A note about abstract classes: If there were any methods defined as abstract, our child class would be required to implement them. But we don’t have to worry about that in this case!) To override transform() and create our own logic, we just copy the method signature from the parent class:

public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property)

Writing the process plugin (our specific data manipulation)

In order to write custom code, let’s review our use case of dates again. Since this is our mapping:

OLD D6 field_date (from component) -> NEW D8 field_date_from
OLD D6 field_date (to component) -> NEW D8 field_date_to

We will migrate field_date twice per node. The first time, we will pull the from date. The second time, we will pull the to date. Since our process plugin needs to be aware of which piece we’re looking for in that particular run, we will allow the process plugin to have additional configuration. In our case, we will call this configuration date_part, which can be either from or to, and defaults to from:

$date_part = isset($this->configuration['date_part']) ? $this->configuration['date_part'] : 'from';

Depending on which date part we’re looking for, we’ll grab the appropriate date from the D6 source field, which is stored in the array $value.

$value = ($date_part == 'from') ? $value['value'] : $value['value2'];

And we’ll return the string, which will populate the new field:

return $value;

That’s it for writing our process plugin! Now we just have to use it.

Using the process plugin

In our migration definition, we need to call this plugin and feed it the correct information. So back in [yourmodulename]/config/install/migrate.migration.d6_node.yml, we map the new and old fields:

field_date_start:
  plugin: custom_date
  source: field_date
  date_part: from
field_date_end:
  plugin: custom_date
  source: field_date
  date_part: to

Which reads like this: For field_date_start on the new site, pass the old field_date to the custom_date process plugin, with the configuration date_part = ‘from’. Do this all again for field_date_end, but with the configuration date_part = ‘to’. Both of our D8 fields get filled out, each getting its data from a single field on the old D6 site.

migration

Time to fly! (Image courtesy of Wikimedia)


Feedback

Hopefully this helps. If you have any corrections, improvements, questions, or links to how you use plugins, leave them in the comments!

Aug 26 2015
Aug 26

We’re working on our first Drupal 8 project here at Advomatic, and Jim and I have been tasked with implementing a content migration from the client’s existing Drupal 6 site.

My first assignment was to write a plugin which rewrites image assist tags in node body fields as regular HTML image tags. Fortunately, lots of smart people had already solved this problem for Drupal 6 to Drupal 7 migrations (I adapted my plugin from Olle Jonsson’s script on Github), so the biggest hurdle was learning how to implement this thing in Drupal 8.

This is the true story of how we made it work.

Note: You’ll need to use Drush 8 for working with Drupal 8. I’d recommend following Karen Stevenson’s great tutorial from the Lullabot blog to help set up multiple versions of Drush on your system.

Initial setup

As of this writing, you’ll need some very specific Git checkouts of Drupal core and the migration helper modules, or you’re going to immediately encounter a pile of fatal errors. These are working for us:

Enable those modules and their dependencies, then set up your own custom module for your plugin code. The very cool Drupal Console module is a quick way to generate the boilerplate files you’ll need.

Write some code

Migration template

Your migration is likely going to need to provide migration templates for various node types, as well as one that handles all nodes. This plugin for handling image assist tags needs to run on all imported nodes, so we start by copying /core/modules/node/migration_templates/d6_node.yml over to our module and adjusting it a little to instruct it to run the ImgAssist plugin (see line 36 here).

Migrate plugin

There are example process plugins in “process” folders around the installation, and looking at those was a great way to figure out how to write ours. Jim made note of these commands to use for finding example code:

find ./ -type d -name 'migration_templates'
find ./ -type d -name 'process'

Our ImgAssist migrate process plugin starts with the Drupal 6 node body and teaser values, and then it runs through a few steps to create their Drupal 8 counterparts:

Running a node migration, step-by-step

  • 1. Get the D6 site running locally.
  • 2. Install Drupal 8 dev at the commit noted above.
  • 3. Install migrate_plus and migrate_upgrade at the commits noted above, and enable your custom module.
  • 4. Add your D6 database connection information to settings.php (you can follow the Drupal 7 directions here).
  • 5. Run these Drush commands:
    • drush8 migrate-upgrade --legacy-db-url=mysql://dbusername:[email protected]/D6databasename --legacy-root=http://d6site.local --configure-only
    • drush8 migrate-status (just to make sure your custom migration template is registering)
    • drush8 migrate-import yourmodule_d6_node

You’ll probably get an error the first time running migrate-import since the node migration depends on a few others to run first, such as d6_user. Run the dependency migrations as needed, then try the custom node import again.

If you have a lot of nodes, the import process will take a few minutes. I actually wrote this entire blog post while waiting for imports to run. Go do something fun for a minute, you’ve earned it.

Eventually, migrate-import will finish running, and you’ll be all set! You can compare the node on your D8 site against the node on the D6 site and see that the tag has been replaced. Hooray!

If it didn’t work: read on. It’s totally fine, you’ve got this.

So what if you have to roll it back?

drush migrate-rollback hasn’t been implemented in D8 just yet (but it is getting close). A workaround is to use drush scr to run a script which deletes your newly-imported nodes. We’ve been using this: https://gist.github.com/sarahg/993b97d6733003814fda

Then, you’ll need to uninstall your custom module, remove all of its config entities from the database, and drop its database tables. You can do that with queries like these:

DELETE from config where name=“migrate.migration.yourmodule_d6_node”;
DROP table migrate_map_yourmodule_d6_node;
DROP table migrate_message_yourmodule_d6_node;

To make this a little easier, you could add these queries to a hook_uninstall function in your module. I’m not one for making things easy (I’m working on this migration before there’s even a Drupal 8 release candidate, after all), so I’ve just been using drush sql-cli.

Now you can adjust your code as needed, re-enable your module and give it another shot (you can just skip ahead to the “drush8 migrate-import yourmodule_d6_node” step at this point).

Further reading

It took a lot of research to figure out how to get migrate working in Drupal 8 this early in the game. These articles were immensely helpful (thanks bloggers and documenters!).

Aug 26 2015
Aug 26

We’re wrapping up our first Drupal 8 project here at Advomatic, and Jim and I have been tasked with implementing a content migration from the client’s existing Drupal 6 site.

My first migration job was to write a plugin which rewrites image assist tags in node body fields as regular HTML image tags. Fortunately, lots of smart people had already solved this problem for Drupal 6 to Drupal 7 migrations (I adapted my plugin from Olle Jonsson’s script on Github), so the biggest hurdle was learning how to implement this thing in Drupal 8.

This is the true story of how we made it work.

Note: You’ll need to use Drush 8 for working with Drupal 8. I’d recommend following Karen Stevenson’s great tutorial from the Lullabot blog to help set up multiple versions of Drush on your system.

Initial setup

When we first ran our migrations, a few months ago, we needed specific checkouts from dev branches of core and migrate modules. However, as of our final migration run yesterday (10/6/15), we were able to use:

Enable those modules and their dependencies, then set up your own custom module for your plugin code. The very cool Drupal Console module is a quick way to generate the boilerplate files you’ll need.

Write some code

Migration template

Your migration is likely going to need to provide migration templates for various node types, as well as one that handles all nodes. This plugin for handling image assist tags needs to run on all imported nodes, so we start by copying /core/modules/node/migration_templates/d6_node.yml over to our module and adjusting it a little to instruct it to run the ImgAssist plugin (see line 36 here).

Migrate plugin

There are example process plugins in “process” folders around the installation, and looking at those was a great way to figure out how to write ours. Jim made note of these commands to use for finding example code:

find ./ -type d -name 'migration_templates'
find ./ -type d -name 'process'

Our ImgAssist migrate process plugin starts with the Drupal 6 node body and teaser values, and then it runs through a few steps to create their Drupal 8 counterparts:

  • 1. Read through the body value and pick out [img_assist] tags.
  • 2. Split those tags into usable pieces.
  • 3. Build the HTML image tag.
  • 4. Replace the original content containing img_assist tags with the rewritten version, using the built-in transform function.

Running a node migration, step-by-step

  • 1. Get the D6 site and your D8 site running locally.
  • 3. Enable migrate_plus, migrate_upgrade and your custom module.
  • 4. Add your D6 database connection information to settings.php (you can follow the Drupal 7 directions here).
  • 5. Run these Drush commands:
    • drush8 migrate-upgrade --legacy-db-url=mysql://dbusername:[email protected]/D6databasename --legacy-root=http://d6site.local --configure-only
    • drush8 migrate-status (just to make sure your custom migration template is registering)
    • drush8 migrate-import yourmodule_d6_node

You’ll get a notice the first time running migrate-import since the node migration depends on a few others to run first, such as d6_user. Run the dependency migrations as needed, then try the custom node import again.

If you have a lot of nodes, the import process will take a few minutes. I actually wrote this entire blog post while waiting for imports to run. Go do something fun for a minute, you’ve earned it.

Eventually, migrate-import will finish running, and you’ll be all set! You can compare the node on your D8 site against the node on the D6 site and see that the tag has been replaced. Hooray!

If it didn’t work: read on. It’s totally fine, you’ve got this.

So what if you have to roll it back?

drush migrate-rollback hasn’t been implemented in D8 just yet (but it is getting close). A workaround is to use drush scr to run a script which deletes your newly-imported nodes. We’ve been using this: https://gist.github.com/sarahg/993b97d6733003814fda

Then, you’ll need to uninstall your custom module, remove all of its config entities from the database, and drop its database tables. You can do that with queries like these:

DELETE from config where name=“migrate.migration.yourmodule_d6_node”;
DROP table migrate_map_yourmodule_d6_node;
DROP table migrate_message_yourmodule_d6_node;

To make this a little easier, you could add these queries to a hook_uninstall function in your module. I’m not one for making things easy (I’m working on this migration before there’s even a Drupal 8 release candidate, after all), so I’ve just been using drush sql-cli.

Now you can adjust your code as needed, re-enable your module and give it another shot (you can just skip ahead to the “drush8 migrate-import yourmodule_d6_node” step at this point).

Further reading

It took a lot of research to figure out how to get migrate working in Drupal 8 this early in the game. These articles were immensely helpful (thanks bloggers and documenters!).

Sep 30 2012
Sep 30

I recently worked on a project which required a updated version of the jQuery library. While there is the jQuery Update module, it only allows you to upgrade Drupal 6 to jQuery 1.3. If you really know what you're doing and want to upgrade beyond that version, you can either hack core or create your own simple module to do it. While hacking core is certainly the easier approach (simply overwriting misc/jquery.js with a newer version), it is very bad practice. You do not want to get yourself in the habit of altering Drupal core unless you want to kill your upgrade path and deal with a new slew of bugs and unpredictable behavior.

Let's start by creating an admin area for configuring the version of jQuery we want to use.

<?php
/**
* Module configuration admin form.
*
*/
function mymodule_admin_form() {
 
$form['mymodule_custom_jquery'] = array(
   
'#type' => 'checkbox',
   
'#title' => t('Override Jquery'),
   
'#description' => t('Replace the version of jQuery that ships with Drupal
      (jQuery 1.2.6) with the jQuery library specified below. You will need to
      flush your cache when turning this feature on or off.'
),
   
'#default_value' => variable_get('mymodule_custom_jquery', 0),
  );
 
$form['mymodule_custom_jquery_file'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Jquery file location'),
   
'#description' => t('Specify the location of the updated jQuery library
      you would like to use. The location is relative to the docroot and
      so should probably begin with "sites/".'
),
   
'#size' => 128,
   
'#default_value' => variable_get('mymodule_custom_jquery_file', NULL),
  );
$form = system_settings_form($form);

  return

$form;
}
?>

Next, we're going to write a validation hook that simply makes sure the file exists.

<?php
/**
* Admin form validation callback.
*
*/
function mymodule_admin_form_validate($form, &$form_state) {
 
$file = trim($form_state['values']['mymodule_custom_jquery_file']);
 
$form_state['values']['mymodule_custom_jquery_file'] = $file; // Only validate this value if js override is turned on
 
if (variable_get('mymodule_custom_jquery', 0)) {
    if (!
file_exists($file)) {
     
form_set_error('mymodule_custom_jquery_file', t('The file you specified does not exist: !file.', array('!file' => $file)));
    }
  }
}
?>

And lastly, we use hook_preprocess_page() to safely replace the jQuery that ships with Drupal core with our own version.

<?php
/**
* Implementation of hook_preprocess_page().
*/
function mymodule_preprocess_page(&$variables) {
  if (
variable_get('mymodule_custom_jquery', 0)) {
   
$file = variable_get('mymodule_custom_jquery_file', 0);
   
$scripts = drupal_add_js(); // remove core jquery and add our own
   
unset($scripts['core']['misc/jquery.js']);
   
$add_scripts['core'][$file] = array(
     
'cache' => TRUE,
     
'defer' => FALSE,
     
'preprocess' => TRUE,
    );
   
$scripts['core'] = array_merge($add_scripts['core'], $scripts['core']);
   
$variables['scripts'] = drupal_get_js('header', $scripts);
  }
}
?>

Make note of the line:

<?php
    $scripts
['core'] = array_merge($add_scripts['core'], $scripts['core']);
?>

We are careful to add our new jQuery include at the beginning of the array (where the original jquery.js include was) in order to meet any jquery dependencies in the scripts that follow.

Mar 06 2012
Mar 06

Last Thursday we had the opportunity to meet up wth Bryan Ollendyke aka btopro to discuss his flagship product ELMS, as well as numerous other modules – Accessible Content for making more standards compliant sites, CDN for speeding up sites, Organic Groups for working with students and classes, Spaces, database tuning, and many more.  You’ll want to skip to ~minute 8 or 9 where we worked out our technical difficulties.  Ask any questions in the blog and I’ll do my best to help answer.  Specifically settings for CDN are covered as well as use cases and other general high level approaches to development.   Note that link goes to a JNLP blackboard elluminate session – you can exand the size of the window pretty easily in there

https://sas.elluminate.com/p.jnlp?psid=2012-03-01.1249.M.31E08D9EFCC408A0F784A02BEE522E.vcr&sid=2008268

apologies for the

 

Jul 04 2010
Jul 04
APC graphs APC entries

There are several different PHP accelerators to choose from, but according to wikipedia "APC is quickly becoming the de-facto standard PHP caching mechanism as it will be included built-in to the core of PHP starting with PHP 6".

I recently put together a new development webserver in a virtualbox virtual machine, and as I was setting it up I thought I'd take the opportunity to test how much difference APC actually makes to a simple Drupal site.

Installation

I was using Ubuntu server. On newer releases APC is available from the package manager...

$ apt-cache search php-apc
php-apc - APC (Alternative PHP Cache) module for PHP 5
$ sudo apt-get install php-apc

...however I'm using Ubuntu 8.04 LTS (Hardy Heron) and there's no apc-php package. It's not hard to install via PECL / PEAR though. First some dependencies need to be installed, then PECL can be used to install APC.

$ sudo apt-get install php-pear php5-dev apache2-threaded-dev build-essential
$ sudo pecl install apc

This last command will produce a ton of output, but one of the last lines will tell you to add this to your php.ini file (which you'll find in /etc/php5/apache2/php.ini) - you'll probably have to do so manually.

extension=apc.so

Restart apache, and you should see a new APC section in phpinfo() which will confirm it's enabled. There's a small php script which gives you some useful info about APC, which you'll find in /usr/share/php/apc.php - you could use a symlink to allow you to get to this file in your browser to see the stats and graphs it produces to tell you what files it has cached, and info on cache hits and misses.

What difference does it make?

I've left the APC default settings - which in my case was only 30mb of memory being used for cache, and run some basic tests using Apache Bench on a simple Drupal 6 site. The actual performance figures are not that important (this is a virtual machine on my laptop, not a production server), but it's interesting to see how much difference it makes turning APC on.

I tested two pages - the very simple homepage, and another page which displays a relatively long webform. The AB command I used was for 100 requests with 10 concurrent requests. e.g.

$ ab -n 100 -c 10 http://mytestsite.example/webform/

test of APC on a Drupal 6 site Test Requests / Second homepage (APC off) 2.78 webform (APC off) 1.72 homepage (APC on) 8.36 webform (APC on) 3.77
test of APC on a Drupal 6 site Test Milliseconds / Request homepage (APC off) 359.68 webform (APC off) 582.94 homepage (APC on) 119.61 webform (APC on) 265.41

You can see that the effect of APC on the simple homepage is more dramatic than on the webform page. This is almost certainly because the database has to do a lot more work to build the latter, and APC's not going to help on that front. However, we can say on the simple page APC makes Drupal perform almost 3 times faster. With the more database-heavy webform page, the improvement is slightly less - but we're still looking at a doubling in performance.

This is obviously not a hugely detailed test, but it certainly leaves me in no doubt that installing APC represents a quick and easy way to achieve a huge improvement in performance for Drupal sites.

Jun 02 2009
Jun 02

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:

  1. The field's columns weren't being populated because the fields' .module files weren't being included.
  2. CCK uses drupal_write_record() to record the field information but it was failing because content_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.

Mar 19 2009
Mar 19

For some work projects we've started making all the configuration changes via update functions. These get checked into version control and from there deployed to the staging site for testing, and then eventually deployed on the production site. The nice thing about update functions is that you can test it on staging and be sure that exactly the same changes will occur on the production site.

Here's a few examples, I'll continue to update it as I get more good examples.

Installing a module

Simple one liner to enable several modules:

<?php
function foo_update_6000(&$sandbox) {
 
$ret = array();
 
drupal_install_modules(array('devel', 'devel_node_access'));
  return
$ret;
}
?>

Batch based update to regenerate PathAuto aliases

More elaborate update that uses the BatchAPI to avoid timeouts while regenerating the path aliases for two node types:

<?php
function foo_update_6000(&$sandbox) {
 
$ret = array();  if (!isset($sandbox['progress'])) {
   
// Set the patterns
   
variable_set('pathauto_node_foo_pattern', 'foo/view/[nid]');
   
variable_set('pathauto_node_bar_pattern', 'bar/view/[nid]');    // Initialize batch update information.
   
$sandbox['progress'] = 0;
   
$sandbox['last_processed'] = -1;
   
$sandbox['max'] = db_result(db_query("SELECT COUNT(*) FROM {node} n WHERE n.type IN ('foo', 'bar')"));
  } 
// Fetch a group of node ids to update.
 
$nids = array();
 
$result = db_query_range("SELECT n.nid FROM {node} n WHERE n.type IN ('foo', 'bar') AND n.nid > %d ORDER BY n.nid", array($sandbox['last_processed']), 0, 50);
  while (
$node = db_fetch_object($result)) {
   
$nids[] = $node->nid;
  }  if (
$nids) {
   
// Regenerate the aliases for the nodes.
   
pathauto_node_operations_update($nids);    // Update our progress information for the batch update.
   
$sandbox['progress'] += count($nids);
   
$sandbox['last_processed'] = end($nids);
  } 
// Indicate our current progress to the batch update system. If there's no
  // max value then there's nothing to update and we're finished.
 
$ret['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);  return $ret;
}
?>

Change node settings

Make a few changes to the node type settings:

<?php
function foo_update_6001() {
 
$ret = array();  // Change the teaser label to 'teaser text'.
 
$ret[] = update_sql("UPDATE content_node_field_instance SET label = 'Teaser text' WHERE field_name = 'field_teaser'");  // Change the 'description' and 'biography' labels to 'body text'.
 
$ret[] = update_sql("UPDATE content_node_field_instance SET label = 'Body text' WHERE field_name IN ('field_description', 'field_bio')");  // Rename the front node type 'Front Page' to 'Front Page Configuration'
 
$ret[] = update_sql("UPDATE node_type SET name = 'Front Page Configuration' WHERE type = 'front'");  return $ret;
}
?>

Delete a bunch of views

I exported the site's views into a default views and needed to remove the existing ones from the database.

<?php
function foo_update_6001(&$sandbox) {
 
$ret = array();  // Since we're shipping default views delete the versions from the database.
 
if (!isset($sandbox['progress'])) {
   
// Initialize batch update information.
   
$sandbox['progress'] = 0;
   
$sandbox['views'] = array(
     
'season',
  
// ...
     
'nodequeue_1',
    );
   
$sandbox['max'] = count($sandbox['views']);
  } 
module_load_include('module', 'views');
 
$view_id = $sandbox['views'][$sandbox['progress']];
  if (
$view = views_get_view($view_id)) {
   
$view->delete();
   
$view->destroy();
  }
 
$sandbox['progress']++;  // Indicate our current progress to the batch update system. If there's no
  // max value then there's nothing to update and we're finished.
 
$ret['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);  return $ret;
}
?>
Feb 06 2009
Feb 06

I wasted more time that I want to admit do trying to figure this out. I was trying theme a specific CCK field named field_images on all the nodes where it appears. The devel_themer module was listing content-field-field_images.tpl.php as a candidate:

But after copying CCK's content-field.tpl.php into my theme and renaming it I couldn't seem to get the theme to pick it up. Roger López gave me the frustratingly simple answer on irc: "i think you need to have both templates in place"... duh. Copied content-field.tpl.php into my theme and everything worked great.

Jul 13 2008
Jul 13

So building on my last post for creating CCK fields, here's some code I whipped up to migrate from the D6's core upload.module to the filefield.module. This isn't general purpose code but might help someone else out. The catch is I'd built a video node with and was using the upload module to attach exactly two files, an image and a video. The new node will have separate thumbnail and video fields. If you'll be moving to a multi-value field this code won't work for you.

The gist is the same as before, setup your field for video and your field for images then export using:

<?php
var_export
(content_fields('field_web_video', 'video'), 1);
?>

and

<?php
var_export
(content_fields('field_video_thumb', 'video'), 1);
?>


Then roll that into an update function that also moves the file data around in the database. Code is after the jump.

<?php
/**
* Add filefields to the video nodes and migrate the files.
*/
function foo_video_update_6000() {
 
// Make sure the filefield* modules are installed correctly.
 
drupal_install_modules(array('filefield', 'filefield_image', 'filefield_imagecache'));
 
drupal_flush_all_caches();  module_load_include('inc', 'content', 'includes/content.admin');
 
content_alter_db_cleanup();
 
 
// Need to load the CCK include file where content_field_instance_create() is defined.
 
module_load_include('inc', 'content', 'includes/content.crud');
 
 
$thumb_field = array (
//
// DROPPED THE CCK FIELD DEFINITION FROM HERE
//
 
);
 
content_field_instance_create($thumb_field);  $video_field = array (
//
// DROPPED THE CCK FIELD DEFINITION FROM HERE
//
 
);
 
content_field_instance_create($video_field); 
 
// Migrate the videos
 
$fids = array();
 
$result = db_query("SELECT n.nid, n.vid, f.fid, u.description, u.list FROM {files} f INNER JOIN {upload} u ON f.fid = u.fid INNER JOIN {node} n ON u.vid = n.vid WHERE n.type = 'video' AND filemime LIKE 'video/%'");
  while (
$file = db_fetch_object($result)) {
   
$fids[] = $file->fid;
   
// Check for a record... it adds a bunch more queries but it's simple and we only run this once.
   
if (db_result(db_query("SELECT COUNT(*) FROM {content_type_video} WHERE vid = %d", $file->vid))) {
     
db_query("UPDATE {content_type_video} SET field_web_video_fid = %d, field_web_video_description = '%s', field_web_video_list = %d WHERE vid = %d",
       
$file->fid, $file->description, $file->list, $file->vid);
    }
    else {
     
db_query("INSERT INTO {content_type_video} (nid, vid, field_web_video_fid, field_web_video_description, field_web_video_list) VALUES (%d, %d, %d, '%s', %d)",
       
$file->nid, $file->vid, $file->fid, $file->description, $file->list);
    }
  }
 
db_query("DELETE FROM {upload} WHERE fid IN (". db_placeholders($fids, 'int') .")", $fids);   // Migrate the images
 
$fids = array();
 
$result = db_query("SELECT n.nid, n.vid, f.fid, u.description, u.list FROM {files} f INNER JOIN {upload} u ON f.fid = u.fid INNER JOIN {node} n ON u.vid = n.vid WHERE n.type = 'video' AND filemime LIKE 'image/%'");
  while (
$file = db_fetch_object($result)) {
   
$fids[] = $file->fid;
   
// Check for a record... it adds a bunch more queries but it's simple and we only run this once.
   
if (db_result(db_query("SELECT COUNT(*) FROM {content_type_video} WHERE vid = %d", $file->vid))) {
     
db_query("UPDATE {content_type_video} SET field_video_thumb_fid = %d, field_video_thumb_description = '%s', field_video_thumb_list = %d WHERE vid = %d",
       
$file->fid, $file->description, $file->list, $file->vid);
    }
    else {
     
db_query("INSERT INTO {content_type_video} (nid, vid, field_video_thumb_fid, field_video_thumb_description, field_video_thumb_list) VALUES (%d, %d, %d, '%s', %d)",
       
$file->nid, $file->vid, $file->fid, $file->description, $file->list);
    }
  }
 
db_query("DELETE FROM {upload} WHERE fid IN (". db_placeholders($fids, 'int') .")", $fids);  // No more uploads on video nodes!
 
variable_set('upload_video', 0);  return array();
}
?>

Update: I posted some additional info on this topic over on: http://drupal.org/node/292904

Jul 02 2008
Jul 02

I spent some time today trying to figure out how to create a CCK field as part of an hook_update_N function. Unlike previous versions of CCK, in 6 it's very easy to manipulate the fields from code.

The first step is to create the field using CCK's UI. Once you've got the field setup the way you'd like it use PHP's var_export() to dump the contents of the node's field as an array:

var_export(content_fields('field_translator_note', 'feature'));

That'll give you some massive array definition that you can copy and paste into your code.

<?php
$field
= array (
 
'field_name' => 'field_translator_note',
 
'type_name' => 'feature',
 
'display_settings' =>
  array (
   
4 =>
    array (
     
'format' => 'hidden',
    ),
   
2 =>
    array (
     
'format' => 'hidden',
    ),
   
3 =>
    array (
     
'format' => 'hidden',
    ),
   
'label' =>
    array (
     
'format' => 'hidden',
    ),
   
'teaser' =>
    array (
     
'format' => 'hidden',
    ),
   
'full' =>
    array (
     
'format' => 'hidden',
    ),
  ),
 
'widget_active' => '1',
 
'type' => 'text',
 
'required' => '0',
 
'multiple' => '0',
 
'db_storage' => '0',
 
'module' => 'text',
 
'active' => '1',
 
'columns' =>
  array (
   
'value' =>
    array (
     
'type' => 'text',
     
'size' => 'big',
     
'not null' => false,
     
'sortable' => true,
    ),
  ),
 
'text_processing' => '0',
 
'max_length' => '',
 
'allowed_values' => '',
 
'allowed_values_php' => '',
 
'widget' =>
  array (
   
'rows' => '',
   
'default_value' =>
    array (
     
0 =>
      array (
       
'value' => '',
      ),
    ),
   
'default_value_php' => NULL,
   
'label' => 'Translator\'s note',
   
'weight' => NULL,
   
'description' => '',
   
'type' => 'text_textarea',
   
'module' => 'text',
  ),
);
// Need to load the CCK include file where content_field_instance_create() is defined.
module_load_include('inc', 'content', 'includes/content.crud');// I wanted to add the field to several node types so loop over them...
foreach (array('athlete', 'feature', 'product', 'tech') as $type) {
 
// ...and assign the node type.
 
$field['type_name'] = $type;
 
content_field_instance_create($field);
}
?>

High-fives to all the CCK developers for making this so easy.

May 05 2008
May 05

This is a little snippet I came up with to get a block to show up on a single node type:

<?php
$menu
= menu_get_item();
if (
$menu['path'] == 'node/%' && isset($menu['page_arguments'][0]->type)) {
  return
$menu['page_arguments'][0]->type == 'story';
}
?>


It uses the node type stored in the menu system so you don't have to match arg(0) etc.

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web