Export content types as code

by Gisle Hannemyr

This chapter describes how to create a module that creates a custom node content type when it is enabled on a Drupal website.

Table of contents

Drupal project discussed in this chapter: Features.

Introduction

In an earlier chapter you've learned how to use the Drupal GUI to create content types using fields. However, sometimes, you may want the content type to be a feature of some module. You install the module, and the content type appear, you uninstall the module, and it is gone. No need to configure anything!

It is possible to create a Drupal content type in code from scratch, but it will save a lot of work if you first populate the content type by first using the Drupal GUI (i.e. FieldUI) to create the fields, then use the Features module to export the fields to a custom Feature-module, and optionally transform this generated module into a stand-alone module.

This chapter introduces version 2 of Features. Please note that this is a very versatile project, and capable of other things than exporting fields (one of its main uses is deployment). But in this chapter, I shall specifically describe how to use it to export a content type created in the GUI as a regular Drupal module.

See also the community documentation at Drupal.org.

Setting up

Before starting, please download and enable the contributed module Features. This module will be used to do generate most of code for the content type. If you haven't done so already, do:

$ drush en features -y

Features is ready to used after being enabled. There is no need to configure anything.

However, the default option for a feature export is to download it as a tarball and save that on your personal computer. If you want to be able to save a feature on your development server, you need to set up a path to where you want the export saved. To do this, first create a directory for saving feature exports in the the sites/all/modules directory. Make it belong to the web server group and make theis group writeable by the web server. Provided you want to call this directory “featuresexport” and the web server group is “www-data”, the following sequence of CLI commands will do this:

$ mkdir featuresexport
$ sudo chgrp www-data featuresexport
$ sudo chmod g+w featuresexport

Then navigate to Structure » Features » Settings, scroll down to the bottom of the screen and under “General settings”, fill in the field for “Default export path” with the path to the directory you created.

Feature export and import

To export a complete content type, navigate to Structure » Features » Create feature. In the General information panel, you should at least fill in the fields “Name” and “Description”. The fields you fill in here will end up where you expect in the generated modules' .info-file. You should chose a name that describes your content type. However, in the examples in this chapter, to keep things simple, I shall assume the name “My module” has been given.

noteThe name of the feature must begin with a letter. The name will be converted to lower case and spaces replaced by underscores to generate the machine name for the export. For example, “My module” becomes my_module. The name will be used in places where a machine name is required (e.g. the machine name for a bundle). Make sure the name used for $field_instances, etc. is the machine name.

You may also fill in some other fields before moving on to the Components panel to select what features you want to export.

features01.png

If you expand Content-types (x) (node), you will see a list of content types. Check the content types you want to export.

tipThis tutorial mainly discusses features export of content types. However, as you can see from the “Components” panel on the screenshot above, other components may be exported.

Provided you've already set up a directory on your development server for saving features exports (recommended). you can expand the Advanced options panel on the left and click on the button “Generate feature”. This saves the feature as a set of files in the designated directory on your development server.

Alternatively, you can click on the “Download feature”-button on the left. This produces a tarball that you can download and save to your local computer.

To define a new node content type, we need to supply hook_node_info(). This hook defines one or more node types in Drupal. It should return an array defining a new node type along with some of its properties. This hook is generated automatically by Features.

To import the exported features on another website, make sure that Features is enabled on the site, and treat the directory holding the export just as any other Drupal project. To import, enable it as you would enable any Drupal module.

Tweaks

The hook_node_info() in my_module.features.inc generated by Features let you set some node options. Example:

/**
 * Implements hook_node_info().
 */
function my_module_node_info() {
  $items = array(
    'my_bundle' => array(
      'name' => t('My bundle'),
      'base' => 'node_content',
      'description' => t('This is a custom node type.'),
      'has_title' => 1,
      'title_label' => t('Title'),
      'has_body' => 0,
      'help' => 'The explanation or submission guidelines for the node type.',
    ),
  );
  drupal_alter('node_info', $items);
  return $items;
}

If you change the base in hook_node_info from the default (node_content), you also need to provide the custom hook_form(). Here is an example (place it in my_module.module):

/**
 * Implements hook_form().
 */
function myformbase_form($node, $form_state) {
  return node_content_form($node, $form_state);
}

Please note that this example just passes the call to the default without changing anything. But such a function may be useful as a placeholder if you want to be able to preprocess the form later.

There are also some node options that cannot be set this way, such as:

There exists some database variables that determine how this is handled. Below are three of those, and the values that can be used.

Comment status:
comment_type
int: COMMENT_NODE_HIDDEN, COMMENT_NODE_CLOSED, COMMENT_NODE_OPEN
Miscellaneous node options:
node_options_type
array: array('status', 'promote', 'sticky', 'revision')
Display author and date information:
node_submitted_type
int: FALSE (hide), TRUE (show)

The usual solution is to set the appropriate database variables in hook_install for the feature export module that creates my_nodetype, e.g.:

variable_set('comment_my_nodetype', COMMENT_NODE_CLOSED);
variable_set('node_options_my_nodetype', array('status'));
variable_set('node_submitted_my_nodetype', FALSE);

See alsoSee also this support request at Drupal.org and this question at Drupal SE.

Exporting fields

Fields that are added through the GUI to existing bundles cannot be exported as content types, but they can be exported as individual fields.

If you expand Field bases (x) (field) and Field instances (x) (field_instance), you will see all the individual fields that exists on your site. Check those you want to export, and then click on Download feature or Generate feature to extract them to download or to files.

The definitions of these fields can then be added unchanged to the arrays that make up the base and instance definitions.

Anatomy of an export

In Drupal 7, the field configuration is split into two different pieces of configuration data: the base field definition, and a specific instance of a field. The base definition must be unique, while instance is repeated when the same base field is reused in other bundles.

The base definition contains the properties that cannot be changed when a field is shared across multiple content types, such as it's basic field-type. The instance contains the properties specific to each content type, such as the label, widget, widget settings, display mode settings, etc. Features provide one file to populate the field bases, and another to populate the field instances.

Given that a new content type with some custom fields has been created in the GUI, creating it as a feature, dowloading it, and unpacking the tarball should produce at least the following five files:

  1. my_module.info – module metadata and dependencies
  2. my_module.module – no real content, but includes my_module.features.inc
  3. my_module.features.inc – Contains hook_node_info()
  4. my_module.features.field_base.inc – field bases definitions
  5. my_module.features.field_instance.inc – field instance definitions

There may be more. For instance, if your feature export defines a taxonomy vocabulary, there will also be:

  1. my_module.features.taxonomy.inc – Contains hook_taxonomy_default_vocabularies()

These files constitute a Drupal module that is ready for use with a Features depen­dency.

noteNo my_module.install is generated. This means that uninstalling the module will have no effect. All content types, content and tables created using the module will remain. Reinstalling after uninstalling will result in a fatal error, as it will try to create tables that already exists in the database. See also these issues: Disabling a feature does not disown content type.

Disabling and uninstalling

According to the documentation (Installing, enabling and disabling bundled Features), disabling a feature is supposed to work like this:

By design, disabling the feature module is supposed to also disable all the functionality that was defined by it. That is, remove node types that were created by it, etc.

It don't. This causes a fatal database error (DatabaseSchemaObjectExistsException) if you uninstall and reinstall a features export module.

features01.png

For production, you should create the missing hook_uninstall. For development, a quick fix is to backup a clean datebase and then drop all tables when reinstalling. A patched version of Backup and Migrate let you do this. You need to apply the patch in #53. See also this bug report/feature request.

Removing the dependency on Features

It is possible to create the code to create a node type bundle from scratch, but it is a lot of work. The hard part is to get all the parameters of the field bases and field instances right.

However, if you use the Drupal GUI to create the new node type, and then export it by turning the node content type and all its fields into a feature, Features will do most of the work for you, but we shall create the module metadata file (mybundle.info) from scratch. It may look like this:

name = My module
description = This is an example of how to create a content type with code.
core = 7.x
package = Other

However, we shall use the following three functions that has been generated by the Features export. Assuming the module is named “My module”, the functions, (and the files they are in) are named as follows::

If Features did not create my_module.install, create it. To make sure our module finds the above three functions, place the following near the top (below <?php) and inital comment of my_module.install:

include_once 'my_module.features.field_base.inc';
include_once 'my_module.features.field_instance.inc';
include_once 'my_module.features.inc';

Also make sure the following is placed near the top of my_module.module:

include_once 'my_module.features.inc';

The base definition may contain references to fields that are already defined, such as body. Delete these from the $field_bases array.

As for base fields you do not intend to reuse, there may be conflicts, in particular if you've picked generic machine names (e.g. field_count). By using the name of your module as part of the field machine name (e.g. field_my_module_count), conflicts are less likely).

To make sure that any conflicts are detected upon install, you may use hook_requirements() to check for conflicts (place it in my_module.install):

function my_module_requirements($phase) {
  $requirements = array();
  $conflicts = array();
  if ('install' == $phase) {
    foreach (my_module_field_default_field_bases() as $field_new) {
      $field_old = field_info_field($field_new['field_name']);
      if ($field_new['field_name'] == $field_old['field_name']) {
        $conflicts[] = $field_new['field_name'];
      }
    }
    if (!empty($conflicts)) {
      $clist = implode(', ', $conflicts);
      $requirements['my_module'] = array(
        'title' => t('title'), 
        'description' => t('my_module: Field %clist already exists.',
          array('%clist' => $clist)), 
        'severity' => REQUIREMENT_ERROR,
      );
    }
  }
  return $requirements;
}

You also need to supply a pair of function to add (and delete) your custom fields. The following will do nicely (place them in my_module.install):

function _my_module_add_custom_fields() {
  foreach (my_module_field_default_field_bases() as $field) {
    field_create_field($field);
  }
  foreach (my_module_field_default_field_instances() as $instance) {
    field_create_instance($instance);
  }
}

function _my_module_del_custom_fields() {
  foreach (array_keys(my_module_field_default_field_bases()) as $field) {
    field_delete_field($field);
  }
  foreach (my_module_field_default_field_instances() as $instance) {
    field_delete_instance($instance);
  }
}

Then implement or add to hook_install():

/**
 * Implements hook_install().
 */
function my_module_install() {
  …
  // Add node types
  node_types_rebuild();

  // Do we need to do anything with the body field?
  // Remove next line if 'body' => 0.
  // node_add_body_field($bundle);
  
  // Add fields
  _my_module_add_custom_fields();
}

Finally, add to or implement hook_uninstall() to clean up the database if the custom content type is uninstalled:

/**
 * Implements hook_uninstall().
 */
function my_module_uninstall() {
  …
  // Delete node content
  $bundles = array();
  foreach (my_module_node_info() as $key => $value) {
    $bundles[] = $key;
  }
  $nodeids = array();
  foreach ($bundles as $bundle) {
    $sql = 'SELECT nid FROM {node} n WHERE n.type = :type';
    $result = db_query($sql, array(':type' => $bundle));
    foreach ($result as $row) {
      $nodeids[] = $row->nid;
    }
  }
  dpm($nodeids, 'nodeids');
  if (!empty($nodeids)) {
    node_delete_multiple($nodeids);
  }
  // Delete fields.
  _my_module_del_custom_fields();
  // Delete bundles.
  foreach ($bundles as $bundle) {
    node_type_delete($bundle);
  }
  node_types_rebuild();
  menu_rebuild();
  field_purge_batch(500);
}

noteThe call to node_type_delete() in in hook_uninstall() may produce a lot of error notices: ”Trying to get property of non-object in comment_node_type_delete”. I believe it is a kernel bug, see Issue #1565892. AFAIK, it is safe to ignore this error. It does not prevent the node types from being deleted.

Taxonomies

The file my_module.features.taxonomy.inc contains hook_taxonomy_default_vocabularies(). This hook is part of Features, so it will not work when you disable Features.

Instead, add any taxonomy vocabularies you need in hook_install(), and delete them in hook_uninstall().

Add taxonomy vocabulary:

  // Add taxonomy vocabulary
  $my_vocabulary = new stdClass();
  $my_vocabulary->name = 'My vocabulary';
  $my_vocabulary->description = 'Description';
  $my_vocabulary->machine_name = 'my_vocabulary';
  $my_vocabulary->module = 'my_module';
  taxonomy_vocabulary_save($my_vocabulary);
  $vid = $my_vocabulary->vid;
  $vocab = taxonomy_vocabulary_machine_name_load('my_vocabulary');
  $term = (object) array(
   'name' => 'Term 1',
   'description' => 'Desc. of Term 1.',
   'weight' => 1,
   'vid' => $vid,
  );
  taxonomy_term_save($term);
  $term = (object) array(
   'name' => 'Term 2',
   'description' => 'Desc. of Term 2.',
   'weight' => 2,
   'vid' => $vid,
  );
  taxonomy_term_save($term);

Delete taxonomy vocabulary and all its terms:

// Delete taxonomy vocabulary and all its terms.
$vocab = taxonomy_vocabulary_machine_name_load('my_vocabulary');
taxonomy_vocabulary_delete($vocab->vid);

Refactoring

Finally, to make the code cleaner and its roots in Features less obvious, do the following refactoring:

  1. Rename my_module.features.field_base.incmy_module.field_base.inc.
  2. Rename my_module.features.field_instance.incmy_module.field_instance.inc.
  3. Rename my_module.features.incmy_module.nodeinfo.inc.

Exporting a view

Closely related to exporting a content type is exporting a view showing aggregates of the content type.

NEEDS CLEANUP

Views are exportable. This means you can create a view using the GUI, and then export it to code. The template below can be used to construct an export file for some custom module (named “MYMODULE” in this example.

<?php

/**
 * @file
 * Views for MYMODULE.
 */

/**
 * Implements hook_views_api().
 */
function MYMODULE_views_api() {
  return array('api' => 3.0);
}

/**
 * Implements hook_views_default_views().
 */
function MYMODULE_views_default_views() {
  $views = [];
  // Paste in code exported here.
  
  // Repeat for each view.
  $views[$view->name] = $view;

  // Only once, at the end.
  return $views;
}

Then, put this just after the opening @file comment in MYMODULE.module:

$path = drupal_get_path('module', 'MYMODULE');
require_once(DRUPAL_ROOT . '/' . $path . '/MYMODULE.views.inc'); 

The view is only read once for import. To reimport the view, uninstall the module, then reinstall it again.

See also

Final word

[TBA]


Last update: 2018-08-13 [gh].