Features: Empowering developers one feature at a time

Nov 22 2012

Features is a popular module that’s been circulating around in the Drupalsphere for over three years. For those unfamiliar, it creates a module out of a set of configurations and entities from a Drupal site, capturing it in code, therefore allowing you to easily recreate the same configurations and entities on another site (or the same site for that matter). A quick search will turn up multiple tutorials on how to get started with site building using Features, such as this very nice three part video tutorial by Code Karate, so we won’t elaborate too much here. For those familair with using Features, I highly suggest reading Lullabot’s post on the backstory behind Features as it illustrates very well the disconnect between the original vision of the module and the now common usecase of deployment across sites.

However, as an avid programmer, Features appeals to me in another way. Features as a site building tool can allow for bypassing Drupal’s intuitive UI and interact with the structure of the site through a different facet. Below I will illustrate two examples of how we have taken advantage of Features’ site building capability.

Multilingual static content entry on a panel

In this situation I needed to place multiple panes of static content on a panel. To place the first language-aware pane we follow the following procedures: 1. click “add content” at desired region 2. click “New custom content” 3. input desired content, via copy-paste or otherwise 4. click the gear to open up the contextual menu for the pane we’ve just created 5. click “add a new rule” under the visibility section 6. choose “User: language” and click next 7. select the appropriate language, English in this case, and click save

As we can see from the above workflow, that’s quite a bit of clicking around and waiting for the ajax to load. Now if we have 6 languages, we’d have to repeat the above 5 more times.

Now if we have this panel nicely packaged in a feature, instead of adding the next pane through the UI, let’s open up the file that corresponds to the panel in the feature. For a panel page it will be named “name_of_feature.pages_default.inc”.

    disabled = FALSE; /* Edit this to true to make a default page disabled initially */    $page->api_version = 1;    $page->name = 'multilingual_panel_page';    $page->task = 'page';    $page->admin_title = 'Multilingual Panel Page';    $page->admin_description = '';    $page->path = 'multilingual-panel-page';    $page->access = array();    $page->menu = array();    $page->arguments = array();    $page->conf = array(      'admin_paths' => FALSE,    );    $page->default_handlers = array();    $handler = new stdClass();    $handler->disabled = FALSE; /* Edit this to true to make a default handler disabled initially */    $handler->api_version = 1;    $handler->name = 'page_multilingual_panel_page_panel_context';    $handler->task = 'page';    $handler->subtask = 'multilingual_panel_page';    $handler->handler = 'panel_context';    $handler->weight = 0;    $handler->conf = array(      'title' => 'Panel',      'no_blocks' => 0,      'pipeline' => 'standard',      'body_classes_to_remove' => '',      'body_classes_to_add' => '',      'css_id' => '',      'css' => '',      'contexts' => array(),      'relationships' => array(),    );    $display = new panels_display();    $display->layout = 'onecol';    $display->layout_settings = array();    $display->panel_settings = array(      'style_settings' => array(        'default' => NULL,        'top' => NULL,        'left' => NULL,        'middle' => NULL,        'right' => NULL,        'bottom' => NULL,      ),    );    $display->cache = array();    $display->title = '';    $display->content = array();    $display->panels = array();      $pane = new stdClass();  //line 62      $pane->pid = 'new-1';      $pane->panel = 'middle';      $pane->type = 'custom';      $pane->subtype = 'custom';      $pane->shown = TRUE;      $pane->access = array(        'plugins' => array(          0 => array(            'name' => 'site_language',            'settings' => array(              'language' => array(                'en' => 'en',                'default' => 0,                'zh-hant' => 0,                'fr' => 0,                'de' => 0,                'ja' => 0,                'ko' => 0,                'ru' => 0,              ),            ),            'not' => FALSE,          ),        ),      );      $pane->configuration = array(        'admin_title' => 'Static content in English',        'title' => '',        'body' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',        'format' => 'filtered_html',        'substitute' => TRUE,      );      $pane->cache = array();      $pane->style = array(        'settings' => NULL,      );      $pane->css = array();      $pane->extras = array();      $pane->position = 0;      $pane->locks = array();      $display->content['new-1'] = $pane;      $display->panels['middle'][0] = 'new-1';  //line 104    $display->hide_title = PANELS_TITLE_FIXED;    $display->title_pane = 'new-1';    $handler->conf['display'] = $display;    $page->default_handlers[$handler->name] = $handler;    $pages['multilingual_panel_page'] = $page;      return $pages;    }  

Now the code of interest to us is from line 62 to 104 in the above code block. This block of code here represents the first pane we’ve created. We can use it as a template to build our next pane. We simply make a copy of the code right below and modify as needed. First we make sure to change the pid (pane id) as to not collide with the existing pane. By default the export function for panels auto-increments the pid from "new-1" onward so the next logical pid would be "new-2", however you may assign any pid you like here. As seen in the modified end result code block below, we’ve changed the pid at line 106, 146, and 147. We also need to change the array index on line 147 from "[‘middle’][0]" to "[‘middle’][1]". Once again the index we’ve changed does not make a difference so long as it does not collide. The ordering of the pane actually derives from the order from which the code is processed.

Now that we’ve taken care of the collisions, we can start changing the properties of interest. The access property of the $pane object starting at line 111 appears to correspond to the language visibility rule we’ve set for the first pane, so let’s adjust it.

Before:

    $pane->access = array(        'plugins' => array(          0 => array(            'name' => 'site_language',            'settings' => array(              'language' => array(                'en' => 'en',                'default' => 0,                'zh-hant' => 0,                'fr' => 0,                'de' => 0,                'ja' => 0,                'ko' => 0,                'ru' => 0,              ),            ),            'not' => FALSE,          ),        ),      );  

After:

    $pane->access = array(        'plugins' => array(          0 => array(            'name' => 'site_language',            'settings' => array(              'language' => array(                'en' =>0,                'default' => 0,                'zh-hant' => 0,                'fr' => ’fr’,                'de' => 0,                'ja' => 0,                'ko' => 0,                'ru' => 0,              ),            ),            'not' => FALSE,          ),        ),      );  

Now to change the actual content of the pane we can edit the array at line 133.

Before:

    $pane->configuration = array(        'admin_title' => 'Static content in English',        'title' => '',        'body' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',        'format' => 'filtered_html',        'substitute' => TRUE,      );  

After:

    $pane->configuration = array(        'admin_title' => 'Static content in French’,        'title' => '',        'body' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',        'format' => 'filtered_html',        'substitute' => TRUE,      );  

Now if we save the file and revert the feature via the UI or drush fr feature-name -y and reload the panel page we can see the new pane appear, with all the right settings.

End result:

disabled = FALSE; /* Edit this to true to make a default page disabled initially */    $page->api_version = 1;    $page->name = 'multilingual_panel_page';    $page->task = 'page';    $page->admin_title = 'Multilingual Panel Page';    $page->admin_description = '';    $page->path = 'multilingual-panel-page';    $page->access = array();    $page->menu = array();    $page->arguments = array();    $page->conf = array(      'admin_paths' => FALSE,    );    $page->default_handlers = array();    $handler = new stdClass();    $handler->disabled = FALSE; /* Edit this to true to make a default handler disabled initially */    $handler->api_version = 1;    $handler->name = 'page_multilingual_panel_page_panel_context';    $handler->task = 'page';    $handler->subtask = 'multilingual_panel_page';    $handler->handler = 'panel_context';    $handler->weight = 0;    $handler->conf = array(      'title' => 'Panel',      'no_blocks' => 0,      'pipeline' => 'standard',      'body_classes_to_remove' => '',      'body_classes_to_add' => '',      'css_id' => '',      'css' => '',      'contexts' => array(),      'relationships' => array(),    );    $display = new panels_display();    $display->layout = 'onecol';    $display->layout_settings = array();    $display->panel_settings = array(      'style_settings' => array(        'default' => NULL,        'top' => NULL,        'left' => NULL,        'middle' => NULL,        'right' => NULL,        'bottom' => NULL,      ),    );    $display->cache = array();    $display->title = '';    $display->content = array();    $display->panels = array();      $pane = new stdClass();      $pane->pid = 'new-1';      $pane->panel = 'middle';      $pane->type = 'custom';      $pane->subtype = 'custom';      $pane->shown = TRUE;      $pane->access = array(        'plugins' => array(          0 => array(            'name' => 'site_language',            'settings' => array(              'language' => array(                'en' => 'en',                'default' => 0,                'zh-hant' => 0,                'fr' => 0,                'de' => 0,                'ja' => 0,                'ko' => 0,                'ru' => 0,              ),            ),            'not' => FALSE,          ),        ),      );      $pane->configuration = array(        'admin_title' => 'Static content in English',        'title' => '',        'body' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',        'format' => 'filtered_html',        'substitute' => TRUE,      );      $pane->cache = array();      $pane->style = array(        'settings' => NULL,      );      $pane->css = array();      $pane->extras = array();      $pane->position = 0;      $pane->locks = array();      $display->content['new-1'] = $pane;      $display->panels['middle'][0] = 'new-1';      $pane = new stdClass();      $pane->pid = 'new-2';  //line 106      $pane->panel = 'middle';      $pane->type = 'custom';      $pane->subtype = 'custom';      $pane->shown = TRUE;      $pane->access = array(        'plugins' => array(          0 => array(            'name' => 'site_language',            'settings' => array(              'language' => array(                'en' => 0,                'default' => 0,                'zh-hant' => 0,                'fr' => 'fr',                'de' => 0,                'ja' => 0,                'ko' => 0,                'ru' => 0,              ),            ),            'not' => FALSE,          ),        ),      );      $pane->configuration = array(        'admin_title' => 'Static content in French',        'title' => '',        'body' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',        'format' => 'filtered_html',        'substitute' => TRUE,      );      $pane->cache = array();      $pane->style = array(        'settings' => NULL,      );      $pane->css = array();      $pane->extras = array();      $pane->position = 0;      $pane->locks = array();      $display->content['new-2'] = $pane;  //line 146      $display->panels['middle'][1] = 'new-2';  //line 147    $display->hide_title = PANELS_TITLE_FIXED;    $display->title_pane = 'new-1';    $handler->conf['display'] = $display;    $page->default_handlers[$handler->name] = $handler;    $pages['multilingual_panel_page'] = $page;      return $pages;    }  

Multiple image styles creation

Let’s say you are working on a project that needs a dozen or so different image styles dimensions. It can be quite a chore to manually create these, so let’s see how we can leverage Feature to expedite this process.

Here’s the generated code from the features.inc file for an image style with the scale and crop effect set to 300x300.

 'scale_crop_300x300',      'effects' => array(        1 => array(          'label' => 'Scale and crop',          'help' => 'Scale and crop will maintain the aspect-ratio of the original image, then crop the larger dimension. This is most useful for creating perfectly square thumbnails without stretching the image.',          'effect callback' => 'image_scale_and_crop_effect',          'dimensions callback' => 'image_resize_dimensions',          'form callback' => 'image_resize_form',          'summary theme' => 'image_resize_summary',          'module' => 'image',          'name' => 'image_scale_and_crop',          'data' => array(            'width' => '300',            'height' => '300',          ),          'weight' => '1',        ),      ),    );      return $styles;  }  

Now here’s the code for an additional image style with also the scale and crop action but set to 400x400.

 'scale_crop_300x300',      'effects' => array(        1 => array(  //line 17          'label' => 'Scale and crop',          'help' => 'Scale and crop will maintain the aspect-ratio of the original image, then crop the larger dimension. This is most useful for creating perfectly square thumbnails without stretching the image.',          'effect callback' => 'image_scale_and_crop_effect',          'dimensions callback' => 'image_resize_dimensions',          'form callback' => 'image_resize_form',          'summary theme' => 'image_resize_summary',          'module' => 'image',          'name' => 'image_scale_and_crop',          'data' => array(            'width' => '300',            'height' => '300',          ),          'weight' => '1',        ),      ),    );      // Exported image style: scale_crop_400x400.    $styles['scale_crop_400x400'] = array(      'name' => 'scale_crop_400x400',      'effects' => array(        2 => array(  //line 39          'label' => 'Scale and crop',          'help' => 'Scale and crop will maintain the aspect-ratio of the original image, then crop the larger dimension. This is most useful for creating perfectly square thumbnails without stretching the image.',          'effect callback' => 'image_scale_and_crop_effect',          'dimensions callback' => 'image_resize_dimensions',          'form callback' => 'image_resize_form',          'summary theme' => 'image_resize_summary',          'module' => 'image',          'name' => 'image_scale_and_crop',          'data' => array(            'width' => '400',            'height' => '400',          ),          'weight' => '1',        ),      ),    );      return $styles;  }  

We can see that the export code for each image style is nice and chunkified which makes it very easy for us to copy and paste to additional image styles with varying dimensions. We just need to watch out for any incrementing index such as on line 17 and 39 and make sure we increase accordingly for each image style we add.

These are just a small taste of the level of manipulation that can be done to Features’ exports, and by proxy, to a Drupal site’s structure. As you get more familiar with each module's Feature exportables you will find more and more ways to directly get at the settings you want and bypass the UI. This approach is one that I find myself turning to more and more as it has saved me many work hours.

Learn from us
Sign up and receive our monthly insights directly in your inbox!

Subcribe to newsletter (no spam)

Fields