Programmatic CCK content type creation

Do you want your Drupal module to create a new content type using the power and flexibility of CCK, perhaps including the built in integration with Views? Here's one way, using Drupal 6.

Step 1: Design your CCK content type using the Drupal content type editor

The menu path is: Administer -> Content management -> Content types -> Add content type, URL http://example.com/admin/content/types/add. For more information about getting started with building a content type with CCK, visit this Drupal handbook page.

Step 2: Export your new CCK content type

After you save your new content type in step 1 above, then navigate to menu path Administer -> Content management -> Content types -> Export, URL http://example.com/admin/content/types/export.

Select the content type you just created from the radio buttons displayed, and then click the Export button.

The next screen displays the fields for the selected content type.

Take note of the Types of fields you used. If there are any which are not included with the basic CCK module (content.module), you need to make sure to include them in the dependencies for your module below, or it will be unable to create the content type.

In general, you will want to use the default of exporting all of the fields. Make sure they are all selected via the checkboxes, then click the Export button.

The next page produces a large textarea with the necessary PHP code to define your CCK content type. When ready to use this code, select all of the text in the textarea and copy it to your copy buffer to paste into your definition function, described below.

Step 3: create a PHP function to hold your CCK definition

The template of the function will look like this, prior to pasting the contents of your copy buffer from above (containing the exported CCK content type definition code). We use a separate function just for the definition because it is must be updated with a new export from CCK if we decide to change or tweak our CCK content type later. It makes editing easier and less error prone.

function _modulename_cck_export() {
  // paste code after this line.

  // paste code before this line.
  return $content;
}

Step 4: Carefully paste the exported CCK content type code into the function

The end result should look like this:

function _modulename_cck_export() {
  // paste code after this line.
  $content[type]  = array (
    'name' => 'computed moodle',
    'type' => 'computemoodle',
    'description' => 'Example CCK code.',
    'title_label' => 'Title',
    'body_label' => 'Body',
    'min_word_count' => '0',
    'help' => '',
    'node_options' => 
    array (
      'status' => true,
      'promote' => true,
      'sticky' => false,
      'revision' => false,
    ),
    'old_type' => '',
    'orig_type' => '',
    'module' => 'node',
    'custom' => '1',
    'modified' => '1',
    'locked' => '0',
    'content_profile' => false,
    'comment' => '2',
    'comment_default_mode' => '4',
    'comment_default_order' => '1',
    'comment_default_per_page' => '50',
    'comment_controls' => '3',
    'comment_anonymous' => 0,
    'comment_subject_field' => '1',
    'comment_preview' => '1',
    'comment_form_location' => '0',
  );
  $content[fields]  = array (
    0 => 
    array (
      'label' => 'Example field',
      'field_name' => 'field_example',
      'type' => 'number_integer',
      'widget_type' => 'number',
      'change' => 'Change basic information',
      'weight' => '-1',
      'description' => '',
      'default_value' => 
      array (
        0 => 
        array (
          'value' => '',
          '_error_element' => 'default_value_widget][field_example][0][value',
        ),
      ),
      'default_value_php' => '',
      'default_value_widget' => NULL,
      'group' => false,
      'required' => 0,
      'multiple' => '0',
      'min' => '',
      'max' => '',
      'prefix' => '',
      'suffix' => '',
      'allowed_values' => '',
      'allowed_values_php' => '',
      'op' => 'Save field settings',
      'module' => 'number',
      'widget_module' => 'number',
      'columns' => 
      array (
        'value' => 
        array (
          'type' => 'int',
          'not null' => false,
          'sortable' => true,
        ),
      ),
      'display_settings' => 
      array (
        'label' => 
        array (
          'format' => 'above',
          'exclude' => 0,
        ),
        'teaser' => 
        array (
          'format' => 'default',
          'exclude' => 0,
        ),
        'full' => 
        array (
          'format' => 'default',
          'exclude' => 0,
        ),
        4 => 
        array (
          'format' => 'default',
          'exclude' => 0,
        ),
      ),
    ),
  );
  $content[extra]  = array (
    'title' => '-5',
    'body_field' => '-3',
    'menu' => '-2',
  );
  // paste code before this line.
  return $content;
}

Step 5: Optionally clean up the pasted code

You may want to edit any string array indexes to be enclosed in single quote marks to avoid PHP warnings. I'm not sure why CCK exports faulty code like this. For example, as exported, the last array element is given as

$content[extra] = array (

but really should be expressed as

$content['extra'] = array (

Step 6: Write PHP code to actually create the content type

Write the install code to initiate creation of the new content type. This is where we make sure we have the necessary CCK pieces, populate a form using the now hard-coded CCK export, and then use drupal_execute() to submit the form to create the new content type. Here's an example:

function _example_install_cck_node() {
  /* get the CCK node types to be created.  This is where you load the 
   * file containing your function from above, if necessary, and then call
   * that function.
   */
  module_load_include('inc', 'modulename', 'modulename.ccknodedef');
  $content = _modulename_cck_export();  // in modulename.ccknodedef.inc

  // CCK content_copy.module may not be enabled, so make sure it is included
  require_once './' . drupal_get_path('module', 'content') 
      . '/modules/content_copy/content_copy.module';

  $form_state['values']['type_name'] = '<create>';
  $form_state['values']['macro'] = '$content = ' . var_export($content, TRUE) . ';';

  // form provided by content_copy.module  
  drupal_execute('content_copy_import_form', $form_state);
  content_clear_type_cache();
}

Step 7: Call the create function from your module

Figure out where you want to call the above install function from. I created an administrator button for creating it, because hook_install() uses the Batch API which is incompatible with the Forms API used to create the content type. Thus, it's not possible to have the type easily created at install time when the module is enabled. See the bug at issue http://drupal.org/node/297972. This bug is actually now fixed in Drupal CVS, but is not yet in a release as of Drupal 6.10.

Step 8: Edit your *.info file

Be sure to add the appropriate dependencies[] clauses in your modulename.info file if your CCK content type is not optional for your module. At a minimum, you will need to specify the CCK module, which is named content.

All done.

That's it. Now your module can create a CCK content type it can use, and which will benefit from all of the other functionality such as Views which works so well with CCK.

Let me know if you have questions, corrections or improvements.

Thanks to Angie Byron (webchick) for pointing me at some existing code which helped me finish figuring out how to do this cleanly.

Comments

Thanks!

This worked perfectly. It appears that the bug you referenced has now been fixed as of 6.12, because it worked for me in the .install file.

Thanks again,
Wayne.

Fixed in 6.11

Core was actually fixed for that bug (http://drupal.org/node/297972) in release 6.11. I am looking into reworking the install bits of my module's code to check for the core version and auto install the CCK type if it is 6.11 or later.

If you want to do the same for your module (or code), you might look at using the PHP version_compare() function, and perhaps the core defined VERSION constant, perhaps like this:

if (version_compare(VERSION, '6.11', 'ge')) {
  // batch API is compatible with drupal_execute()
}

Could you post (or email me)

Could you post (or email me) a working example of this. I tried to get it to work but have not been successful. Thank you.

Thank you!

This is EXTREMELY useful, thanks for giving such a thorough description. One question:
Now that we are at Drupal version 6.12, is it safe to create the new content type in the hook_install() function?

Yes

Yes, see this comment about that subject: http://tinpixel.com/node/53#comment-1734

In the PHP

In the PHP code
$form_state['values']['type_name'] = '';
should be
$form_state['values']['type_name'] = '<create>';

I dug through the content_copy code to figure out this value needed to be set; only then it hit me why it was empty string on your page.

Also, in Drupal 6.12 the content type creation works fine in hook_install(), so no need for the button described in Step 7.

Now, what about uninstall?

Thank you for posting this how-to--great help.

Uninstallation

For hook_uninstall() you can simply do
node_type_delete('YOUR_CONTENT_TYPE');

Thanks for the follow up

Thanks for the follow up improvements and information. Hopefully they will help other people who are tackling this kind of project with CCK.

Fixed HTML entities < and >

I think I fixed the missing pieces of the PHP code. Darn HTML entities! Let me know if you see any other errors and I will fix them.

hook_install() complications

If you rely on Drupal to automatically enable your module's dependencies, you will run into a problem with hook_install(). The dependencies are not immediately made available to hook_install(), so you will get a WSoD complaining about a function not existing. This is a Drupal issue; see http://drupal.org/node/365098 for details and a workaround using hook_requirements(). The workaround essentially errors out of the install and tells the user to enable the module a second time, now that its dependencies have been enabled.

I'd provide my workaround code but the pre tag is not allowed and the code tag doesn't preserve leading whitespace.

There is an additional problem case even after implementing the workaround: If you have file fields or image fields in the content type you are creating, and you have previously installed FileField or ImageField but they are now disabled, you will get errors about missing functions if you rely on Drupal to automatically enable them when you install your module. This presumably happens because those modules rely on hook_init() to include other code within them, but hook_init() only gets called upon page request. i.e., it doesn't happen between when the module is enabled and when your hook_install() runs. Directly calling hook_init() for both ImageField and FileField from hook_install() in my module fixed the problem.

Would like to see workaround

I'd like to see your workaround code, if you happen to pass by my site again. I realize this response is way after yours. My site is getting hammered with comment spam and so comments are moderated. But it takes forever to wade through them by hand and publish them, which is why yours only got published recently, despite having been written months ago. My apologies.

I added the <pre> tag to the allowed tags, so feel free to use it. Sorry it wasn't already there. I'm running a pretty default install of Drupal here at the moment.

I'm getting good ol' "An

I'm getting good ol' "An illegal choice has been detected" when I try this. I'm guessing something's missing from $form_state - where are you getting that array from? Or are you implicitly declaring it when you first reference it (shame on you)?

After experimentation, it

After experimentation, it looks like it was the info that the Search Restrict module was putting into the content type definition that was causing the problem. After deleting that section from the definition text, everything went fine. If I had a minute, I'd file an issue in Search Restrict's issue queue, but I've got to get the rest of this job done…

Thanks so much Chris

Thanks so much Chris.

I been looking around everywhere for something like this.

A lot of existing information provided out there do not give the complete picture.

Thanks so much.

I made a link to this on

http://drupal.org/node/101742#comment-1882198

Hope you don't mind.

cheers
Ed

No problem

I'm glad this helped a few people out. The link is no problem -- and explains the increase in traffic my sleepy, forgotten little web site has gotten. :-)

Works like a charm

Drupal 6.13 + CCK 2.5,

This is working well within hook_install, and the CCK export code didn't seem to need any cleaning up.

Inefficient?

Thanks for this. Very useful.

One question though: why do you invoke the code to generate the content as an array, and then serialize it again? Couldn't you just load the content from a file (and avoid having to add the code header and footer) as a string?

Forms API

I'm not 100% sure I know what you are asking.

But if you are referring to the var_export($content) call, it's just the way the CCK and Forms APIs want things. It probably is inefficient, but it's only done once on most sites.

Will this cause a problem with existing types?

Will this cause a problem with existing types? I haven't run this on my production server yet, but I'm curious about what would happen if I run this and the type already exists.

CCK types should all have

CCK types should all have unique names. If for some reason the names conflict, I'm not sure what will happen, but it could potentially cause problems, yes.

I've heard you can use the

I've heard you can use the export feature for CCK to get the code required to create a content type for a module. This is the first place I've seen that explains HOW to do this.

Then there is the question of views... would I approach embedding views into a module in the same way?

Thanks!