Drupal is a free and open source content management system written in PHP. The standard release of Drupal, Drupal core, contains basic features typical to content management systems, such as menu management, page layout customization and content publishing. However, its main strength are its community-contributed addons, known as modules, which make it possible to alter and extend Drupal’s capabilities to one’s wishes.
This article is about implementing such a module. There are other good tutorials on this. This one is going to cover the specific case of creating a text file on the server with the content of certain nodes every time a node is created, updated or deleted. The tutorial delves into some of the most important Drupal APIs, such as the Database, File and Form APIs.
People reading this should have added a module to a Drupal installation before and be somewhat familiar with PHP.

1. Drupal Modules

A module is a collection of php functions that link into Drupal, providing additional functionality to the Drupal installation.
Because the module code executes within the Drupal context, it can access all the variables and structures and use all the functions of Drupal core.

1.1. Choose a short name

The first step in creating a module is to choose a short name for it. This short name will be used in all file and function names in your module. In this example, we will be using “nodes_to_text_file” as the short name.

Make sure your short name starts with a letter and only contains lower-case letters and underscores. Also make sure your module does not have the same short name as any theme you will be using.

1.2. Create the directory

In the modules directory of your Drupal 7 install, create a folder with your short name as its name.

1.3 empty module file

Create a nodes_to_text_file.module file in your nodes_to_text_file directory. It is to this file that we will add the functions that will hook into Drupal. In the next section, we will add the first function.
Module files begin with the opening php tag. Add it to the module file:

<?php

However, omit the closing php tag ?>. Omitting it is a convention, and including it might cause strange runtime behavior on certain server setups.

1.4 Creating the info file

Every module has its info file. It contains metadata about the module and hence, tells Drupal about the module. Without this file, the module will not appear in the module listing of your Drupal installation.

Create a nodes_to_text_file.info file in your nodes_to_text_file directory:

name = Nodes To Text File
description = A module that creates a text file containing all content of certain types of nodes whenever a node of one of these types is added, edited or deleted
core = 7.x

The above 3 properties form the minimal setup: name sets the name of the module, description describes what the module does and core determines with which version of Drupal this module is meant to be used.

After adding the info file, the module should be visible in your module listing. Go to Modules and scroll down to the “Other” section. The Nodes To Text File module should be listed there. Enable it.

2. Module Hooks

Hooks are the most important concept to grasp when implementing Drupal modules. Hooks provide the points at which you can insert your actions. They can be thought of as event listeners. An event such as deleting a node would trigger the hook “hook_node_delete”. In this case, Drupal will scan all modules for functions with the name mymodule_node_delete, where mymodule is the module’s name. If your module implements hook_node_delete, that function will always execute after a node gets deleted.

For a list of all available hooks in Drupal core, check the Drupal 7 hooks documentation.

2.1 Implementing the hook_help hook

The first hook we are going to implement is hook_help. This hook should always be implemented. It provides help and information about the module. To implement an hook, you need to replace “hook” in the hook name(f.e. hook_help) with your module’s short name and create a function in the .module file with that name.
In this case, nodes_to_text_file_help:

/**
 * Implements hook_help.
 *
 * Displays help and module information.
 *
 * @param path
 *   Which path of the site we're using to display help
 * @param arg
 *   Array that holds the current path as returned from arg() function
 */
function nodes_to_text_file_help($path, $arg) {
  switch ($path) {
    case "admin/help#nodes_to_text_file":
      return '<p>'.  t("A module that creates a text file containing all content of certain types of nodes whenever a node of one of these types is added, edited or deleted") .'</p>';
      break;
  }
}

The path parameter contains the information about where the user is currently at. If the user is currently at the help section of our module, we return a paragraph that gives information about our module. Note that we use the Drupal t($string) function, which translates the content if possible and which should be used on any string that is shown to the user.

Now, check Modules page of your Drupal site again. You should see a link ‘Help’ on the right of your module. If you click it, you will see the paragraph returned by the nodes_to_text_file_help function of our module.

2.2. Implementing the hooks hook_node_insert, hook_node_update and hook_node_delete

We will now implement the hooks hook_node_insert, hook_node_update and hook_node_delete.
All these hooks will call the same function nodes_to_text_file_write_file($node):

/**
 * Implements hook_node_delete.
 */
function nodes_to_text_file_node_delete($node){
	nodes_to_text_file_write_file($node);
}

/**
 * Implements hook_node_update.
 */
function nodes_to_text_file_node_update($node){
	nodes_to_text_file_write_file($node);
}

/**
 * Implements hook_node_insert.
 */
function nodes_to_text_file_node_insert($node){
	nodes_to_text_file_write_file($node);
}

For now, add the following implementation of the function:

function nodes_to_text_file_write_file($node){
	watchdog('We should write the file again!','Info Message');
}

watchdog is a useful function to debug hooks. They will be added to your Drupal server logging at Reports>Recent log messages:

<watchdog screenshot>

3. Database API

For this app, we will need two queries. One that fetches all the published nodes of the given content types of which the user is the owner:

/**
 * Custom content function.
 *
 * Retrieve data from database
 *
 * @return
 *   A result set of the targeted nodes.
 */
function nodes_to_text_file_contents($contentTypes){
  //Use Database db_select API to retrieve nodes.
  global $user;
  $uid = $user->uid;

  $queryResult = db_select('node', 'n')
    ->fields('n', array('nid', 'title', 'created'))
    ->condition('uid', $uid) //owner is current user.
    ->condition('status', 1) //Published.
    ->condition('type', $contentTypes, 'in') //Node type is in $contentTypes
    ->orderBy('created', 'DESC') //Most recent first.
    ->execute();
  return $queryResult;
}

This query uses the new(starting from Drupal 7) db_select function of the Database API. Other than the old db_query function, db_select makes it possible to construct the query in an object oriented matter.

db_select maps to a sql select statement(as do db_insert, db_update and db_delete map to their sql equivalents). It takes a table name(f.e. ‘node’) and an alias (f.e. ‘n’) as its arguments. This comes down to:

select * from node as n

fields selects the fields listed in the array in its second argument on the alias n:

select nid, title, created from node as n

condition adds a condition to the query. The first is the field, the second the value, the third the operator:

select nid, title, created from node as n where published = 1 and type in ('article','page') and uid = 1

if the values in the $contentTypes array are ‘article’ and ‘page’ and user Steffen has uid 1. Note that the “global $user” statement gets the global $user variable, which contains all information about the user that is currently logged in.

orderBy then sort the results according to the field in its first argument:

select nid, title, created from node as n where published = 1 and type in ('article','page') and uid = 1 order by created asc

The execute method then compiles and runs the query and returns the result.

The other query we need – one that fetches all content types of the core Node Module – uses the same API and is quite simular:

/**
 * Function that retrieves the content types from the database.
 *
 * Retrieve content types from database
 *
 * @return
 *   A result set of content types.
 */
function nodes_to_text_file_retrieve_content_types(){
  $queryResult = db_select('node_type', 'nt')
    ->fields('nt', array('type', 'name'))
    ->condition('module', 'node') //only node content types
    ->orderBy('type', 'ASC')
    ->execute();
  return $queryResult;
}

4. Drupal File API

We are now implementing our nodes_to_text_file_write_file function that is called by the node_insert, node_update and node_delete hooks. We briefly discuss the part in which we determine whether the file should be written and in which we fetch the contents first.

/**
 * Custom write file function.
 *
 * Creates the text file and stores it on the server.
 */
function nodes_to_text_file_write_file($node){
	$contentTypes = variable_get('nodes_to_text_file_content_types', array());
	$nodeType = $node->type;

	$elementFound = false;
	foreach ($contentTypes as &$value) {
    	if ($nodeType === $value){
    		$elementFound = true;
    		break;
    	}
	}

	if ($elementFound){
    	$queryResult = nodes_to_text_file_contents($contentTypes);

    	global $user;
    	$username = $user->name;

   	...
}

First, we fetch the node content types which should be included in the file using variable_get($name,$default) As we will see in the next section, variable_set($name,$value) – typically invoked in a module configuration form – sets Drupal system wide variables, which can then be fetched by variable_get.
We then match the node type of the node that was inserted, updated or deleted with the array of content types. If a match is found($elementFound), execution continues. We fetch the nodes using the query of the previous section and get a reference to the username of the currently logged in user.

Then – where the … dots are in the above function – we add the following and write out the fetched content to a file:

	if (! file_prepare_directory(file_build_uri($username))){
    		drupal_mkdir(file_build_uri($username));
    	}

    	$filename = file_build_uri($username . '/content.txt');
    	$fh = fopen($filename, 'w') or die("can't open file");

   	 	while($record = $queryResult->fetchAssoc()) {
    		$stringData = $record['nid'] . ' - ' . $record['title'] . ' - ' . $record['created'];
    		fwrite($fh, $stringData."\n");
    	} 

    	fclose($fh);

We write the fetched contents to a context.txt file in the default Drupal public files location, in a folder with the username as its name. If this folder does not exist yet, we create it first:

if (! file_prepare_directory(file_build_uri($username))){
   drupal_mkdir(file_build_uri($username));
}

Then we construct the filename:

$filename = file_build_uri($username . '/content.txt');

If the username is “Steffen”, then the filename will be:
<drupal installation location>/sites/default/files/steffen/content.txt

Then we open the file for writing and iterate over the queryResult. We print out the nid, the title and the created timestamp of every record to a new line in the file. We then close the file again:

   $fh = fopen($filename, 'w') or die("can't open file");

   while($record = $queryResult->fetchAssoc()) {
      $stringData = $record['nid'] . ' - ' . $record['title'] . ' - ' . $record['created'];
      fwrite($fh, $stringData."\n");
   } 

   fclose($fh);

We are using some functions of the Drupal File API here:
file_build_uri: This function constructs, given a relative path, a URI into Drupal’s default files location(this is usually something like ‘sites/default/files/’).
file_prepare_directory: This function checks that the directory exists and is writable.
drupal_mkdir: This function creates a directory using Drupal’s default mode.

To respectively open the file for writing, write to it and close it again, we use the php functions fopen, fwrite and fclose.

5. Drupal Form API

Our module is implemented now. However, we want to be able to configure the node content types for which the file should be written. We already saw that we were fetching this array of content types like this:

$contentTypes = variable_get('nodes_to_text_file_content_types', array());

What remains is to explain how we can set this array of content types first through a module configuration form.

We will implement hook_menu so we get a configuration form at our module at the relative path admin/config/content/nodes_to_text_file:

/**
 * Implements hook_menu().
 */
function nodes_to_text_file_menu() {
  $items = array();  

  $items['admin/config/content/nodes_to_text_file'] = array(
    'title' => 'Nodes To Text File',
    'description' => 'Configuration for Nodes To Text File module',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('nodes_to_text_file_form'),
    'access arguments' => array('access administration pages'),
    'type' => MENU_NORMAL_ITEM
  );

  return $items;
}

Using the hook_menu hook, modules register paths to define how URL requests are handled. In this case, we are setting a path that makes a configuration form available from the Drupal Configuration page:

We passed the form already to the array of the ‘page arguments’ property of the link, but we didnt define it yet. We are doing so now:

/**
 * Form function, called by drupal_get_form()
 * in nodes_to_text_file_menu().
 */
function nodes_to_text_file_form($form, &$form_state) {
	$queryResult = nodes_to_text_file_retrieve_content_types();

	$options = array();
	while($record = $queryResult->fetchAssoc()) {
    	$options[$record['type']] = t($record['name']);
    } 

  	$form['nodes_to_text_file_content_types'] = array(
    	'#type' => 'checkboxes',
    	'#title' => t('Content types to include'),
    	'#default_value' => variable_get('nodes_to_text_file_content_types', array()),
    	'#description' => t('The content types to include.'),
    	'#options' => $options
  	);

  return system_settings_form($form);
}

First we are creating the array of possible content types so we can use it later to generate the necessary checkboxes on the form:

$queryResult = nodes_to_text_file_retrieve_content_types();

	$options = array();
	while($record = $queryResult->fetchAssoc()) {
    	$options[$record['type']] = t($record['name']);
    }

In Drupal, forms should be constructed using the Form API. We are using it to construct our form:

$form['nodes_to_text_file_content_types'] = array(
    	'#type' => 'checkboxes',
    	'#title' => t('Content types to include'),
    	'#default_value' => variable_get('nodes_to_text_file_content_types', array()),
    	'#description' => t('The content types to include.'),
    	'#options' => $options
  	);

after which we return the form. Note that the Form API is pretty straightforward. It is generally enough to consult the API page to know what types of fields are available and which properties are available and which ones are required.

6. The entire module file for reference

<?php
/**
 * Implements hook_help.
 *
 * Displays help and module information.
 *
 * @param path
 *   Which path of the site we're using to display help
 * @param arg
 *   Array that holds the current path as returned from arg() function
 */
function nodes_to_text_file_help($path, $arg) {
  switch ($path) {
    case "admin/help#nodes_to_text_file":
      return '<p>'.  t("A module that creates a text file containing all content of certain types of nodes whenever a node of one of these types is added, edited or deleted") .'</p>';
      break;
  }
}

/**
 * Implements hook_node_delete.
 */
function nodes_to_text_file_node_delete($node){
	nodes_to_text_file_write_file($node);
}

/**
 * Implements hook_node_update.
 */
function nodes_to_text_file_node_update($node){
	nodes_to_text_file_write_file($node);
}

/**
 * Implements hook_node_insert.
 */
function nodes_to_text_file_node_insert($node){
	nodes_to_text_file_write_file($node);
}

/**
 * Custom write file function.
 *
 * Creates the text file and stores it on the server.
 */
function nodes_to_text_file_write_file($node){
	$contentTypes = variable_get('nodes_to_text_file_content_types', array());
	$nodeType = $node->type;

	$elementFound = false;
	foreach ($contentTypes as &$value) {
    	if ($nodeType === $value){
    		$elementFound = true;
    		break;
    	}
	}

	if ($elementFound){
    	$queryResult = nodes_to_text_file_contents($contentTypes);

    	global $user;
    	$username = $user->name;

   	 	if (! file_prepare_directory(file_build_uri($username))){
    		drupal_mkdir(file_build_uri($username));
    	}

    	$filename = file_build_uri($username . '/content.txt');
    	$fh = fopen($filename, 'w') or die("can't open file");

   	 	while($record = $queryResult->fetchAssoc()) {
    		$stringData = $record['nid'] . ' - ' . $record['title'] . ' - ' . $record['created'];
    		fwrite($fh, $stringData."\n");
    	} 

    	fclose($fh);
	}
}

/**
 * Custom content function.
 *
 * Retrieve data from database
 *
 * @return
 *   A result set of the targeted nodes.
 */
function nodes_to_text_file_contents($contentTypes){
  //Use Database db_select API to retrieve nodes.
  global $user;
  $uid = $user->uid;

  $queryResult = db_select('node', 'n')
    ->fields('n', array('nid', 'title', 'created'))
    ->condition('uid', $uid) //owner is current user.
    ->condition('status', 1) //Published.
    ->condition('type', $contentTypes, 'in') //Node type is in $contentTypes
    ->orderBy('created', 'DESC') //Most recent first.
    ->execute();
  return $queryResult;
}

/**
 * Function that retrieves the content types from the database.
 *
 * Retrieve content types from database
 *
 * @return
 *   A result set of content types.
 */
function nodes_to_text_file_retrieve_content_types(){
  $queryResult = db_select('node_type', 'nt')
    ->fields('nt', array('type', 'name'))
    ->condition('module', 'node') //only node content types
    ->orderBy('type', 'ASC')
    ->execute();
  return $queryResult;
}

/**
 * Form function, called by drupal_get_form()
 * in nodes_to_text_file_menu().
 */
function nodes_to_text_file_form($form, &$form_state) {
	$queryResult = nodes_to_text_file_retrieve_content_types();

	$options = array();
	while($record = $queryResult->fetchAssoc()) {
    	$options[$record['type']] = t($record['name']);
    } 

  	$form['nodes_to_text_file_content_types'] = array(
    	'#type' => 'checkboxes',
    	'#title' => t('Content types to include'),
    	'#default_value' => variable_get('nodes_to_text_file_content_types', array()),
    	'#description' => t('The content types to include.'),
    	'#options' => $options
  	);

  return system_settings_form($form);
}

/**
 * Implements hook_menu().
 */
function nodes_to_text_file_menu() {
  $items = array();  

  $items['admin/config/content/nodes_to_text_file'] = array(
    'title' => 'Nodes To Text File',
    'description' => 'Configuration for Nodes To Text File module',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('nodes_to_text_file_form'),
    'access arguments' => array('access administration pages'),
    'type' => MENU_NORMAL_ITEM
  );

  return $items;
}