Monday 28 February 2011

The trouble with block_view_MODULE_DELTA_alter

If you want to modify a specific menu block using hook_block_view_MODULE_DELTA_alter() you are out of luck. Let's say you have a menu called "Footer menu" and you want to change its styling so you set up a function like this:

function mymodule_block_view_menu_menu_footer_menu_alter() {}

That is not a mistake: the first "menu" is the module name, and the menu module adds "menu" to the front of the menu name converted to lower case, so the delta for my "Footer menu" is "menu-footer-menu".

But this function will never get called (as of 7-rc4). Why? Because the menu module uses hyphens in the delta name, and the block module does not convert these hyphens to underscores so it tries to call: mymodule_block_view_menu_menu-footer-menu_alter() - which will fail, of course.

You have two choices: Either use only hook_block_view_alter() and check you've got the right block before changing it, or patch block.module until it gets fixed in core.

I've already issued a bug report here http://drupal.org/node/1076132 with the fix, so you can use it if you want to.

Tuesday 22 February 2011

Proper way to get field values

EDIT: download the module that encapsulates this behaviour and does a lot more besides: read this blog.

If you haven't noticed the stored structure of fields in an entity, like a node, is not simple:

$entity ->fieldname[language][delta] = [item]

The Field API does contain useful functions for manipulating these values, so if you have a $node and a field_myfield, you can get the current items like this:

$items = field_get_items('node', $node, 'field_myfield', $node->language);

Which returns an array of the items in the field, indexed by their delta value.

The actual content of each item will be dependent on what it is, a simple textfield will have array('value' => [text]) while a term reference will have array('tid' => [tid]), a "longtext with summary" (such as the standard "body" field) has several values: for the main body ('value'), for the summary ('summary'), for the filter format ('format'), and already processed "safe" values of both the body (safe_value) and the summary (safe_summary).

You can find this, and other useful functions, at Field API.

EDIT: I now have a super-duper function that extracts values for you here.

Monday 21 February 2011

drupal_write_record() and properties

It would be nice if you could use your own classes and objects with drupal_write_record(), but you can't - that's if you want to have private or protected properties.

Let's say you have this class:

class myClass {
  protected $xid, $nid, $mydata;


  public function __construct($nid, $mydata, $xid = 0) {
    $this->xid = $xid;    $this->nid = $nid;
    $this->mydata = $mydata;
  }

  public function __get($property) {
    if (property_exists($this, $property) {
      $value = $this->$property;
    }
    return $value;
  }

  public function __set($property, $value) {
    switch ($property) {
      case 'xid':
         // Don't allow xid to be set, it's read-only
         break;
      default:
        if (property_exists($this, $property) {
          $this->$property = $value;
        }
    }
  }

  public function __isset($property) {
    return isset($this->$property);
  }

  public function save() {
    if ($this->xid) {
      $this->update();
    }
    else {
      $this->insert();
    }
  }

  protected function insert() {
    drupal_write_record('mydata_table', $this);
  }

  protected function update() {
    drupal_write_record('mydata_table', $this, 'xid');
  }
}

This won't work because the drupal_write_record() function uses 'property_exists()' to check for properties and not 'isset()', and that will not return TRUE for private or protected properties.

One of the features of OOP is encapsulation, making data that should not be accessible, not accessible but that means you cannot use drupal_write_record(). If you want to use that function directly with your own classes you have to make the properties public - defeating the point of encapsulation.

However the solution is not too bad, you can do this:

  protected function insert() {
    $object = (object) array();
    foreach ($this as $field => $value) {
      $object->$field = $value;
    }

    drupal_write_record('mydata_table', $object);
    $this->xid = $object->xid;
  }

  protected function update() {
    $object = (object) array();
    foreach ($this as $field => $value) {
      $object->$field = $value;
    }

    drupal_write_record('mydata_table', $object, 'xid');
  }

If truth be told, for the 'insert()' to work you'd need to make 'xid' publicly accessible anyway (since drupal_write_record() wants to write to it), so perhaps this solution is for the best.

Saturday 19 February 2011

field_delete_field ooops

I'm in the process of converting a D6 module to several D7 module.

The original required manual set up of a node type and attaching various CCK fields before being able to run - a perfectly OK scenario for Drupal 6. However for D7 I wanted the module to create the node and attach the fields automagically - a perfectly acceptable scenario for Drupal 7. (I've learned only too well recently that entities, though powerful, require a lot of effort so that conversion can wait.)

One thing I've done is extracted the specialist field I need into its own module - in a similar fashion to the various field modules (text, number and so on).

So I put it together and installed on a minimum D7 installation. All good. I uninstall and then reinstall - and en exception gets thrown because the DB tables for my new field have not been renamed. Drupal 7, for speed, renames discarded tables then deletes them at its leisure during hook_cron.

I know this process works because I've seen it in action - when I've been creating fields through the User Interface. But it was failing during hook_uninstall() - in other words field_delete_field() just fails completely but without reporting an error.

Turns out this is a known bug. And it's because when a module is disabled its fields are tagged as inactive and the routine that collects the file_info does not collect information about inactive fields.

The only solution is to use this function from the field.install file:


function _update_7000_field_delete_field($field_name) {
  $table_name = 'field_data_' . $field_name;
  if (db_select($table_name)->range(0, 1)->countQuery()->execute()->fetchField()) {
    $t = get_t();
    throw new Exception($t('This function can only be used to delete fields without data'));
  }
  // Delete all instances.
  db_delete('field_config_instance')
    ->condition('field_name', $field_name)
    ->execute();


  // Nuke field data and revision tables.
  db_drop_table($table_name);
  db_drop_table('field_revision_' . $field_name);


  // Delete the field.
  db_delete('field_config')
    ->condition('field_name', $field_name)
    ->execute();
}

There is a patch on the go for fixing but this will have to do until it arrives.

Wednesday 16 February 2011

Allowing and disallowing dates in Date Popup

I have just uploaded a patch for the Date Popup module which allows dates to be allowed and disallowed in the popup calendar.

You can find it here.

This was a critical issue for the current commercial project I'm working on, and after having my knuckles wrapped by KarenS for complaining and not helping - I helped.

Sunday 13 February 2011

Blocks and hook_hook_info()

As an ongoing attempt to keep the amount of code loaded for any given page down to a minimum the Drupal 7 developers came up with hook_hook_info() - which is a completely different hook from the one in Drupal 6.

What this hook does is allow a module to define a "group" for its hooks, so the system itself defines the group "tokens" for any token-related hook.

Which means that, when attempting to find implementations of the hook the core checks to see whether a module has a file modulename.tokens.inc - and if it does, it loads that before looking for the token hook. (And records if it finds it in the .inc file.)

This is all good and allows modules to put its token-related hooks in a file that isn't loaded unless its needed.

But 'tokens' is the only group defined for core hooks. Which seems a bit of a waste.

So one thing I've done is to intercept hook_hook_info_alter() and my own group for blocks, like this:


/**
 * Implements hook_hook_info_alter().
 */
function mymodule_hook_info_alter(&$hooks) {
  static $myhooks = array(
    'blocks' => array('block_configure', 'block_view_alter', 'block_view', 'block_save', 'block_list_alter', 'block_info_alter', 'block_info')
  );
  foreach ($myhooks as $group => $items) {
    foreach ($items as $hook) {
      $hooks[$hook] = array('group' => $group);
    }
  }
}

And that's all you need. So you can now happily put your block-related hooks into modulename.blocks.inc. This is not future-proofed, you should really check to see whether the hook has been defined already before doing this, just to be completely safe.

One point worth noting is that there is now a hook_view_BLOCK_DELTA_alter() as well, which you can't put in a group specifically because you'd need an entry for every defined block. However it's not a problem because that hook is called immediately after hook_view_alter() - which will ensure that the .inc file is loaded first.

Don't bother trying to put node hooks into a separate file - it won't work because the node module just goes its own way and doesn't use the hook_info data.

Tuesday 8 February 2011

Extending Chaos Tools Wizard

The Chaos tools form wizard is a very nice piece of code but I had the need to extend it by adding a control button - which turned out to be a lot easier than you might think.

Adding the control button itself was simple enough:



  $form['buttons']['update'] = array(
    '#type' => 'submit',
    '#value' => t('Update'),
    '#next' => $current_step,
    '#wizard type' => 'update',
    '#weight' => -500,
  );

But notice I have given this button a wizard type of "update" (as opposed to 'next', 'cancel' or 'finish').

Now you can either add a new line to your $form_info array:


$form_info['update callback'] = 'mywizard_update';

Or not, in which case the function $form_info['id'] . '_update' will be called.

Your function will get called when this button is clicked with &$form_state as the parameter. Cool.

As an additional point, I'm using 'update' here which means that I do want to validate the form entries and save the values. However by adding these element attributes:


'#limit_validation_errors' => array(),
'#submit' => array('ctools_wizard_submit'),

You can prevent all validation and ensure the proper Chaos tools wizard function is executed.

Thursday 3 February 2011

Fieldsets and Vertical tabs

The CSS for the vertical tabs, seen mostly on node forms, does something quite unpleasant - it hides all <legend> elements. This is fine when it's converting fieldsets into vertical tabs, but it assumes that every fieldset  should be vertical tab - which is not true.

A quick fix for this, especially if you're using Seven as your admin theme, is to add this simple definition:

div.vertical-tabs .vertical-tabs-panes .fieldset-wrapper legend {
  display:block;
  margin-bottom:2.5em;
}
I also found that the legend seems to overwrite the top of the fieldset contents so required the margin-bottom. YMMV.

Wednesday 2 February 2011

Your very own date field

So here's the problem: you have your own hand-built form and you want to include a date field.

The difficulty that I keep running into with Drupal 7 is that everything is tailored around entities - the whole (amazing) Field API - requires an entity to operate on.

But I just want a configuration page with a date field (or in my case a whole bunch of date fields).

Well I have researched long and hard and come up with something very very easy:

$form['date'] = array(
  '#type' => 'date_popup',
);

Which will do it all for you. Watch out for the help text that says it should be "date-popup" because that's wrong. Your default date should be in the form "YYYY-MM-DD HH:MM:SS", you can also set timezones and lots of other useful stuff.

Find the date_popup.module file and search for "date_popup_element_info". It gives you all the defaults for this element type.