Handling Assets for Custom Module in Drupal

[UPDATE Aug 22, 2014]There is newer version of this article “Import Export Sample Data And Assets For Kickstart All Via Features” and a Drupal module samle_data at Drupal site. It is also in the git repo DesignsSquare Lib Sample Data at branch ‘7.x-1.x’

You have custom module a plugin that you have made exportable via Features module. This custom module has sample kickstart data that contains assets such as images, video, etc. In this post, we cover how to manage these assets, so when user enables your custom module, those are copied accordingly and automatically. Furthermore, we provide a custom module on github to handle assets for you so you don’t have to ever worry about assets import/export for you sample kickstart data. At last, the assets referenced directly from content is also packaged for auto transfer whenever a custom module is enabled.

Env: DRupal:7.26, Features:7.x-2.0, UUID:7.x-1.0-alpha5+17-dev, Feature_UUID:7.x-1.0-alpha3+15-dev

About uuid_features_file_path

Each artifact exported via Features module receives an attribute – uuid_features_file_path. This attribute specifies where are the artifact located which is used at the time of import(when module is enabled). We are going to set this attribute at the time of export, so it is pointing to our artifact location. Its important to note, we like to keep our assets part of module dir that way all plugin info is in one dir easier to deliver and import for user.

Set uuid_features_file_path At Export

We are going to hook into feature export process via MODULE-NAME_uuid_node_features_export_render_alter(&$export, $node, $module) as following:

function MODULE-NAME_uuid_node_features_export_render_alter(&$export, $node, $module){
    if($module == 'MODULE-NAME'){
        //setting artifact source location so it imports accordingly
        $field_instance = field_get_items('node', $node, 'field_ARTIFACT-FIELD-NAME');
        $export->field_ARTIFACT-FIELD-NAME['und'][0]['uuid_features_file_path'] = drupal_get_path('module','MODULE-NAME').'/imports/'.$field_instance[0]['filename'];
...
}

Here, we first ensure the code is executed for our module by having IF condition. Afterward, we retrieve field containing artifact for particular Node instance. Next, we update the export attribute uuid_features_file_path with a path to the custom module and path to artifact

This will export the custom module sample date into file with uuid_features_file_path specifying assets located in our custom module folder. There is nothing else to do, because at the import, features module will take care of coping the assets from location specified in uuid_features_file_path to the public dir of that Drupal instance in folder assets “uri” is set to

Forget About Assets…

In the above solution, a user is constrained to have the module placed in the exact location and named accordingly as specified in the uuid_features_file_path, otherwise, it will break. How about forgetting about assets of the sample kickstart data, so you can focus building features. Here is a Drupal module that does just that:
https://github.com/kapasoft-config-scripts/designssquare-lib-assets

Once enabled, this module does the following:

  • Handles all assets of the sample data being exported/imported via Features module
  • Besides Drupal core fields, it also works for custom fields referencing assets
  • It stores the assets relative to the module, so user can name the exportable feature or place it as they wish
  • The assets need to be handled by ‘file_managed’ functionality in order for the module to work

With this module enabled, you should never need to worry about assets any more.

How About Assets Referenced From Content …In Sample Data

So far, we have handled asset export/import for cases where the asset is referenced by some field in a node, however. Most of the time, your sample data will have content that will also reference assets(i.e.

<img src="/sites/default/files/img/some.jpg" alt="thumbnail">

). We also like to export/import those assets, so the sample data doesn’t break on clients Drupal instance. Here are steps to accomplish this

1. Make Part of Custom Module

Make sure the assets is part of the custom module directory…let, say the assets are in ‘modules/CUSTOM-MODULE/assets/img’ directory

2. Generate Array of Assets To Import

Run the following script in the ‘PHP Executable’ block to generate an array of assets to export/import:

include_once DRUPAL_ROOT . '/includes/utility.inc';
include_once DRUPAL_ROOT . '/includes/file.inc';

$MODULE_NAME = 'CUSTOM-MODULE-NAME';
$PATH_TO_MODULE = drupal_get_path('module', $MODULE_NAME);
//source dir or location of the files to import relative to the module specified by variable - $MODULE_NAME
$BASE_SOURCE_REL_PATH = '/assets/img';
//the destination folder relative to the public dir of Drupal instance
$DESTINATION_DIR = '/img';

$output = "function _assets_list(){ \n";
$output .= "\$module_dir = drupal_get_path('module', '" . $MODULE_NAME . "');\n";
$output .= "     return array (\n";

$output .= write_file_array('', $MODULE_NAME, $PATH_TO_MODULE, $BASE_SOURCE_REL_PATH, $DESTINATION_DIR);

$output .= "     );\n";
$output .= "}\n";
drupal_set_message("<textarea rows=100 style=\"width: 100%;\">" . $output . '</textarea>');

function write_file_array($rel_path = '', $MODULE_NAME, $PATH_TO_MODULE, $BASE_SOURCE_REL_PATH, $DESTINATION_DIR)
{

    $output = '';
    $full_path = $PATH_TO_MODULE . $BASE_SOURCE_REL_PATH . $rel_path;
    if ($handle = opendir($full_path)) {
        while (false !== ($file = readdir($handle))) {
            if (is_dir($full_path . '/' . $file) && $file != '.' && $file != '..') {
                $output .= write_file_array('/' . $file, $MODULE_NAME, $PATH_TO_MODULE, $BASE_SOURCE_REL_PATH, $DESTINATION_DIR);
            } elseif ($file != '.' && $file != '..') {
                $uri = file_build_uri($DESTINATION_DIR . $rel_path . '/' . $file);
                $output .= "     '" . $uri . "' => array(\n";
                $output .= "     'uri' => '" . $uri . "',\n";
                $output .= "     'filename' => '" . $file . "',\n";
                $output .= "     'source' => \$module_dir.'" . $BASE_SOURCE_REL_PATH . $rel_path . '/' . $file . "',\n";
                $output .= "),\n";
            }
        }
        closedir($handle);
    }
    return $output;
}

In line 4, we specify our custom module that contains the assets to export. In line 7, we specify the exact directory of assets to export relative to our custom module. In line 9, we specify the destination folder to import the assets relative to the public directory of the Drupal instance. In another words, if the sample content references image via tag

<img src="/sites/default/files/img/some.jpg" alt="thumbnail">

then the destination dir is ‘/img’ assuming the Drupal public dir is ‘sites/default/files’. After running this script, it will generate function ‘_assets_list()’ that contains all the assets to export/import and is going to be used part of the installation script as described next step

3. Transfer assets via module installation script

Next, we configure our Custom Module installation script CUSTOM-MODULE-NAME.install to transfer assets whenever the module is enabled

function CUSTOM-MODULE-NAME_install(){
    $t = get_t();

    //looping through the files and coping to the destination
    foreach(_assets_list() as $file){
        $uri = $file['uri'];
        $dir_name = drupal_dirname($uri);
        file_prepare_directory($dir_name,FILE_CREATE_DIRECTORY);
        (!file_exists(drupal_realpath($uri))) ? file_unmanaged_copy($file['source'], $uri) : '';
    }
    drupal_set_message($t('CUSTOME MODULE NAME assets transferred'));
}

/************COPY _assets_list() HERE*******/

NOTE: Please, copy the function _assets_lists() from the step 2 in the same file CUSTOM-MODULE-NAME.install

This script is run when custom module is enabled. It runs through each asset and transfers from the CUSTOM-MODULE/img into the Drupal/public/img directory. Afterwards, those assets are available when referenced from the sample content

4. Clean on Uninstall

You may also like to remove these assets whenever the custom module with sample data is uninstalled, then add the following in CUSTOM-MODULE-NAME_uninstall() in the CUSTOM-MODULE-NAME.install file:

function CUSTOM-MODULE-NAME_uninstall(){
    $t = get_t();

    //looping through the files and removing one by one
    foreach(_assets_list() as $file){
        $uri = $file['uri'];
        $filename = drupal_realpath($uri);
        (file_exists($uri)) ? file_unmanaged_delete($filename) : '';
    }
    drupal_set_message($t('CUSTOM-MODULE-NAME assets removed'));
}

This script is run on module uninstall and it runs through the same list of assets from _assets_list() and removes each asset from the public dir

Summarize

The attribute uuid_features_file_path specifies the location from where to transfer assets at the time of import, so we hooked into the export and updated this attribute to a location of our assets. We keep it in the same custom module dir for easy delivery and import by user. Furthermore, there is a module developed in github that will handle all assets of the sample data kickstart for you. At last, we also go over how to export/import assets referenced by sample data from the content itself

References

http://drupalcontrib.org/api/drupal/contributions!features!features.api.php/7

How To Implement Custom Field Types In Drupal

In this post, we go over on how to create, handle and display a custom fields in Drupal. In addition, we look on situations when your custom field contains an artifacts that requires an additional care

The hook_field_schema() defines the columns. The table will be only added after an actual field is created(attached to content type). So, if you like to see if the data is being added for the custom field, look into table ‘field_xxx’

The hook_field_shema() needs to be define and declared in the module .install file as following:

function NAME-OF-SCHEMA_field_schema($field) {
    $schema = array();
    $schema['columns']['class'] = array(
        'type' => 'varchar',
        'length' => 50,
        'not null' => FALSE
    );
  
    $schema['columns']['image'] = array(
        'description' => 'The {file_managed}.fid being referenced in this field.',
        'type' => 'int',
        'not null' => FALSE,
        'unsigned' => TRUE,
    );

    $schema['foreign keys'] = array(
        'image_fid' => array(
            'table' => 'file_managed',
            'columns' => array('image' => 'fid'),
        ),
    );

    return $schema;
}

In the above example, a schema of 2 fields are created. One of the fields are a file upload, so there is additional foreign constrain defined.

Test from PHP Execute block

$field = field_info_field('field_designssquare_slider_layer');
module_load_install('field_designssquare_slider_layer');
$schema = (array) module_invoke('field_designssquare_slider_layer', 'field_schema', $field);

Look into Watchdog logs for a message. You should see the message if the install was performed

The columns can also be checked in the table ‘field_NAME_OF_FIELD’ in database

Specify Display

You may like to create a display format for the custom field. The field display gives user ability to select display for the field. Developer can take it and create separate layout via custom theme functions wrapped into template file

To declare display:

/**
 * Implements hook_field_formatter_info().
 */
function videojs_field_formatter_info() {
  return array(
    'videojs' => array(
      'label' => t('Video.js : HTML5 Video Player'),
      'field types' => array('file', 'media', 'link_field'),
      'description' => t('Display a video file as an HTML5-compatible with Flash-fallback video player.'),
      'settings'  => array(
        'width' => NULL,
        'height' => NULL,
        'posterimage_field' => NULL,
        'posterimage_style' => NULL,
      ),
    ),
  );
}

The ‘field types’ specify to which types the display is available. The ‘settings’ is custom parameter passed to the display(will cover later). So, this declares the display available for user to specify from UI. Next, lets implement the display:

/**
 * Implements hook_field_formatter_view().
 */
function videojs_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  if ($display['type'] !== 'videojs') {
    return array();
  }
  if (empty($items)) {
    return array();
  }

  if ($field['type'] == 'link_field') {
    //for field type of 'link_field' do things differently
  }

  $settings = $display['settings'];
  $attributes = array();
  if (!empty($settings['width']) && !empty($settings['height'])) {
    $attributes['width'] = intval($settings['width']);
    $attributes['height'] = intval($settings['height']);
  }


  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
  return array(
    array(
      '#theme' => 'videojs',
      '#items' => $items,
      '#player_id' => 'videojs-' . $id . '-' . str_replace('_', '-', $instance['field_name']),
      '#attached' => videojs_add(FALSE),
      '#entity' => $entity,
      '#entity_type' => $entity_type,
      '#attributes' => $attributes,
      '#posterimage_style' => !empty($settings['posterimage_style']) ? $settings['posterimage_style'] : NULL,
    ),
  );
}

The display formatter calls a custom function declared as following:

function videojs_theme() {
  return array(
    'videojs' => array(
      'variables' => array('items' => NULL, 'player_id' => NULL, 'attributes' => NULL, 'entity' => NULL, 'entity_type' => NULL, 'posterimage_style' => NULL),
      'template' => 'theme/videojs',
    ),
}

Here, the theme custom function used for rendering display is declared. This theme custom function uses a template file ‘theme/videojs’ to render output, but before that the hook_preprocess_CUSTOM-THEME-FUNC is called, so the variables can be preprocessed.

Render the Display

To render specified display as following:

$video_render_array = field_view_field(
                        'node',
                        $entity,
                        'field_video_file',
                        array(
                            'type' => 'videojs',
                            'label' => 'hidden',
                            'settings' => array(
                                  'width' => '300',
                                  'height' => '400',
                                  'posterimage_field' => NULL,
                                  'posterimage_style' => NULL,
                            ),
                        )
                    );
 $content = render($video_render_array);

Here the field ‘field_video_file’ uses a display ‘jw_player’ that takes one parameter jwplayer_preset. Afterwards, the renderable array returned from the display is being rendered.

Create Custom Field Type

To declare custom field type use hook_field_info() as following:

function MODULE-NAME_field_info() {
 return array(
        'OUR_CUSTOM_FIELD_NAME' => array(
            'label' => t('Slide Layer'),
            'description' => t('field containing info about one of the layers part of the Rev Slider'),
            'settings' => array('max_length' => 255),
            'instance_settings' => array(
                'text_processing' => 0,
            ),
            'default_widget' => 'OUR_CUSTOM_FIELD_NAME_widget',
            'default_formatter' => 'OUR_CUSTOM_FIELD_NAME_formatter',
        ),
    );
}

Besides name and description, the default widget and formatter is specified. Next, lets declare and implement the widget for this field type :

/**
 * Implements hook_field_widget_info().
 * Expose Field API widget types.
 */
function MODULE-NAME_field_widget_info() {
    return array(
        'OUR_CUSTOM_WIdGET_NAME' => array(
            'label' => t('Revolution Slide Layer'),
            'field types' => array('OUR_CUSTOM_FIELD_NAME'),
        ),
    );
}

The widget name is given and assigned to our custom field type – OUR_CUSTOM_FIELD_NAME via ‘field types’. After declaring the widget, lets specify the form for user to enter values:

/**
 * Implements hook_field_widget_form().
 */
function MODULE-NAME_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
    $element += array(
        '#type' => $instance['widget']['type'],
        '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
    );
    return $element;
}

/**
 * Implements hook_element_info().
 * Declare the field Form API element types and specify their default values.
 */
function MODULE-NAME_element_info() {
    $elements = array();
    $elements['OUR_CUSTOM_WIdGET_NAME'] = array(
        '#input' => TRUE,
        '#process' => array('designssquare_rev_slider_layer_field_process')
    );
    return $elements;
}

In the function hook_field_widget_form(), it specifies the widget type, so we can use hook_element_info for specifying form. Afterwards, the hook_element_info() is used to specify function returning the renderable array with columns of the schema from the .install file

At last, the widget form is specified:

function designssquare_rev_slider_layer_field_process($element, $form_state, $complete_form) {
    $element['layer_header'] = array(
        'layer_header' => array(
            '#type' => 'markup',
            '#weight' => 0,
            '#markup' => '<p>' . t('Slide Layer '.($element['#delta'] + 1)) . '</p>',
        ),
    );
$element['image'] = array(
        '#title' => t('Image'),
        '#type' => 'managed_file',
        '#weight' => 8,
        '#description' => t('Upload a file, allowed extensions: jpg, jpeg, png, gif'),
        '#default_value' => isset($element['#value']['image']) ? $element['#value']['image'] : '',
        '#upload_location' => REV_SLIDER_DEST.'/img',
        '#states' => array(
            'visible' => array(
//                ':input[title="content_choice_'.$element['#delta'].'"]' => array('value' => 'image'),
                ':input[name="content_choice['.$element['#delta'].']"]' => array('value' => 'image'),
            ),
        ),
        '#upload_validators' => array(
            'file_validate_extensions' => array('jpg jpeg png gif'),
            // Pass the maximum file size in bytes
            'file_validate_size' => array(MAX_SIZE_LIMIT_DS*1024*1024),
        ),

    );
    // To prevent an extra required indicator, disable the required flag on the
    // base element since all the sub-fields are already required if desired.
    $element['#required'] = FALSE;

    return $element;
}

We still have to declare and implement hook_field_is_empty($item, $field), so it know when the field is considered to be empty

function MODULE-NAME_field_is_empty($item, $field)
{
    return !isset($item);
}

Here, we tell that empty is when no instance of our custom field is present

Validating Custom Field Values

To validate the custom field, we first register our custom validation function via hook_formt_alter():

function MODULE-NAME_form_alter(&$form, &$form_state, $form_id)
{
    switch ($form_id) {
        case 'OUR-FORM-ID':
            //assign validation
            $form['#validate'][] = 'some_custom_validate';
            //assign submition
            $form['#submit'][] = 'some_custom_submit';
            break;
    }
}

Since this is a general hook, we filter out only the form of our interest. (For finding Form ID, please, see post ‘Forms In Drupal Overview‘). Afterwards, we specify our custom function for validating the form

Next, the validation logic is declared:

function some_custom_validation(){
 if(empty($form_state['values']['some_field']['und'])){
         form_set_error('some_field[und]', t("error message comes here"));
}

On validation, our custom validation function is being called in which we perform validation for our custom field values

Handling Artifacts For Custom Field

If your custom field contains files, then there is extra care of keeping them permanent as well as removed on deletion. In the above section “Validating Custom Field Values”, we registered our submission processor function. By default all artifacts via file_manage is saved temporary. Next, we ensure the artifacts are permanent on creation and deleted on the remove event:

function some_custom_submit($form, &$form_state)
{
  
        if ((isset($form_state['clicked_button']) && $form_state['clicked_button']['#value'] == "Delete")) {
            //custom field needs to be deleted
            //remove artifact image
            $file = file_load(['values']['our_custom_field']['image']);
            if ($file) {
                file_delete($file);
                file_usage_delete($file, 'MODULE-NAME');
            }
        } else {
            //custom field needs to be added
            //make permanent image file
            $file = file_load(['values']['our_custom_field']['image']);
            if ($file) {
                // Change status to permanent.
                $file->status = FILE_STATUS_PERMANENT;
                //all permanent files needs an entry in the 'file_usage' table
                $id = (isset($form_state['node']->nid)) ? $form_state['node']->nid : 0;
                file_usage_add($file, 'MODULE-NAME', $form_state['node']->type, $id);
                // Save.
                file_save($file);
            }
        }
    }
}

First, we check to see if the node is being deleted or created. Afterwards, we load the artifact and make it permanent or remove it if being deleted

See post ‘Handling Assets For Custom Module In Drupal’ for how to automize artifact import/export

Handling Multiple Instances of Custom Field

In case, user chooses your custom field to be more than one time for specific node, then there is module field_remove_item that allows easy for them to remove any of the instance, however. You will have to handle the artifacts again. That easy to do via hook_field_validate()

function MODULE-NAME_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors)
{
    //remove artifacts  on 'remove item' bottom
            foreach ($items as $key => $item) {
               //see if any of the layers is selected to be deleted
                if ($item['field_remove_item']) {
                    //remove image
                    $file = file_load($item['image']);
                    if ($file) {
                        file_delete($file);
                        file_usage_delete($file, 'MODULE-NAME');
                    }
                }
            }
}

Here, we loop through each instance of your custom field since we don’t know exactly which of the one is being removed. The instance with field ‘field_remove_item’ set, is the one we have to remove artifacts, so we have if statement following with standard way of removing asset managed with manage_file

Uninstalling Custom Fields

If you developed a custom field and installed as a separate module than it will create a lock where you will not be able to uninstall the module. There are two solutions that i am aware. The quick and dirty is to update table ‘fields_config’ to remove the dependency. Another solution is to create a separate module for the purpose to run uninstall hook during which it removes the field, thus, removing the lock so it becomes available to be uninstalled. Lets look at the last solution

Here is the hook_uninstall() for uninstalling the custom module:

function MODULE-NAME_uninstall()
{
    //remove fields from content types
    foreach(_get_bundles_for_field_type('CUSTOM-FIELD-TYPE') as $bundle){
        field_attach_delete_bundle('node', $bundle);
        drupal_set_message('uninstalled field for bundle:'.$bundle);
    }

    //remove tables
    drupal_uninstall_schema('CUSTOM-FIELD-SCHEMA');

    // remove variables
    db_query("DELETE FROM {variable} WHERE name LIKE 'CUSTOM-FIELD-SCHEMA_%'");

    // Remove assets.
//    file_unmanaged_delete_recursive(file_default_scheme() . '://slider');

    // Clear the cache.
    field_info_cache_clear();
}

The function ‘_get_bundles_for_field_type’ returns all the bundles referencing the custom field. Afterwards, we use field_attach_delete_bundle API To remove our custom field type as well as all of its instances. Next the schema is removed, the variables deleted and files removed. At last, we clear the cache

Here is the implementation of _get_bundles_for_field_type():

//returns all the bundles of entity instances that references a field instance with a given type
function _get_bundles_for_field_type($field_type)
{
    $field_map = field_info_field_map();
    $bundles = array();
    foreach ($field_map as $field_name => $item) {
        if ($item['type'] == $field_type) {
            foreach ($item['bundles'] as $bundle_list) {
                foreach ($bundle_list as $bundle) {
                    $bundles[] = $bundle;
                }
            }
        }
    }
    return array_unique($bundles);
}

Render The Field

Troubleshooting

1. PDOException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry ‘public://some/path/img.jpg’ for key ‘uri’: INSERT INTO {file_managed}

This issue came up when our custom field was a file and by reinstalling the field it created a duplicate entry in the manage_file table. The solution was clean file_manage table when instance with our custom field was delete

//clean file_manage database on field instance delete
function MODULE-NAME_node_delete($node){
    if($node->type == 'CERTAIN-TYPE'){
        //removes images from file_manage/file_usage tables
        $field_instance = field_get_items('node', $node, 'field_FIELED-NAME');
        foreach($field_instance as $key => $instance){
            $file = file_load($instance['image']);
            file_delete($file);
            file_usage_delete($file, 'MODULE-NAME');
  ...
}

The hook-node_delete($node) is called when any node is being deleted. Here, in line 5, we look up our custom fields for the node being deleted and then clean the file entry in file_manage and file_usage tables.

This also removes the images from Drupal public directory(by default sites/default/files). We did made files being copied when module is being enabled:

function MODULE-NAME_uuid_node_features_rebuild_alter(&$node, $module){
...
            $source = _get_source_file($final_file->uri);
            file_unmanaged_copy($source, $final_file->uri);
...
}

function _get_source_file($uri){
    $source = drupal_get_path('module','MODULE-NAME').'/imports/public/'.substr($uri, 9);
    if(!file_exists($source)){
        watchdog(WATCHDOG_NOTICE, 'MODULE-NAME Import: Image does not exists at '.$source);
    }
    return $source;
}

The hook_uuid_node_features_rebuild_alter is covered in the article Make Custom Field With File Exportable In Drupal Features. We just extend it with file transfer when module feature is being enabled. In highlighted line, we copy from predefined location to a Drupal public folder.

See also post “Handling Artifacts for Custom Module In Drupal” for solution on handling assets such as images, files

2. Drupal claim “Field type(s) in use”

This message is displayed when you would like to disable the module but Drupal doesn’t permit. To solve problem, go to ‘field_config’ table and update the ‘type’ and ‘module’ text for your custom field. More info on the issue https://drupal.org/node/1284380

Fields pending deletion

This comes up after the custom field is removed but the chrom job run only once which is not sufficient. It needs to run several times.

Reference:

  • http://clikfocus.com/blog/how-set-custom-field-type-using-drupal-7-fields-api
  • http://drewpull.drupalgardens.com/blog/drupal-7-field-api-sample
  • http://drupaltutorial4u.blogspot.com/2011/04/how-to-create-custom-field-for-drupal-7.html
  • https://drupal.org/project/examples
  • http://drewpull.drupalgardens.com/blog/drupal-7-custom-field-formatter
  • http://drupal.stackexchange.com/questions/49958/hook-field-schema-does-not-work
  • http://nodesforbreakfast.com/article/2012/02/24/create-custom-form-api-elements-hookelementinfo-drupal-7
  • https://api.drupal.org/api/drupal/includes%21database%21schema.inc/group/schemaapi/7
  • http://www.slideshare.net/zugec/fields-in-core-how-to-create-a-custom-field

Module Development in Drupal

In this post, we cover different aspects for developing modules in Drupal including creating modules, setting variables and other

Creating Module

Modules location

  • The /modules folder that is reserved for core is not good location for your custom modules
  • The good location is /sites/all/modules folder to keep the contributed and custom modules(/sites/all/modules/contr or /sites/all/modules/custom)

Modules Naming

  • keep lower case
  • its not good practice to include numbers

->the modules description along the name and core version is stored in .info files as following:

name = Sample Module
description = sample module description
core = 7.x
version = 7.x-0.1-dev ;to show up in the admin ui
package = Block Wrapper
files[] = nameOfModule.module
dependencies[] = module1
configure = admin/config/moduleName ;this adds configuration button next to module name in modules list

In order to show up in core admin UI, need .module file. It can be empty but required. Documentation can be included

/**
*@file
*Custom functionally for your custom module
*/

Try, clear cache and check to install the module from admin/modules interface

Create Module Admin Interface

Once installed, next we create an admin UI interface to be able edit configurations and settings of the custom module. To do so, we are going to use hook_config_menu:

function moduleName_config_menu(){
    $items = array();

    //Admin config group
    $items['admin/config/kapasoft'] = array(
        'title' => 'Kapasoft',
        'description' => 'Configure Interface with Driver',
        'access arguments' => array('administer kapasoft configurations'),
    );

    //admin configuraiton - settings
    $items['admin/config/kapasoft/manage'] = array(
        'title' => 'Kapasoft configuration',
        'description' => 'Manage Kapasoft Interface config settings',
        'access arguments' => array('administer kapasoft configurations'),
        'page callback' => 'drupal_get_form',
        'page arguments' => array('kapasoft_config_form'),
    );

    return $items;
}

Here we create the module’s admin page – ‘manage’ in admin UI section ‘admin/config/kapasoft’. Next, clear the cache and see if it appears in the admin menu

You should see an error, because in the ‘page callback’ we specify function that builds a form but its not yet declared. Lets do it now

Admin Menu Form

Here, we create a form displayed in admin UI for our module to manipulate settings. In last step, we specified the function(i.e kapasoft_config_form) that builds the form. Now, we implement it as follows:

function kapasoft_config_form($nodes, &$form_state){
    $form = array();

    $form['overview'] = array(
        '#markup' => t('This interface allows to manage KapaSoft modules Settings'),
        '#prefix' => '<p>',
        '#suffix' => '</p>',
    );

    $form['interface_config'] = array(
        '#title' => t('Interface Config'),
        '#description' => t('Configurations of Driver'),
        '#type' => 'fieldset',
        '#collapsible' => TRUE,
        '#collapsed' => FALSE,
    );

    $form['interface_config']['driver_url'] = array(
        '#title' => t('Driver Url'),
        '#description' => t('url of the driver'),
        '#type' => 'textfield',
        '#default_value' => 'localhost',
        '#required' => TRUE,
    );

    $form['interface_config']['driver_port'] = array(
        '#title' => t('Driver Port'),
        '#description' => t('port of the driver'),
        '#type' => 'textfield',
        '#default_value' => '3000',
        '#required' => TRUE,
    );

    $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Save'),
    );
    return $form;
}

Here we add two fields and set default values. Afterwards, clear the cache and go to the admin UI (admin/config/kapasoft/manage) to see the form with default settings.

Make Data Persistent

One of the ways Drupal provides ability to persist almost any type of data are through the variable_get, variable_set and variable_del API. Good for settings but shouldn’t be used for content

Lets, adjust our default values:

function kapasoft_config_settings_form($nodes, &$form_state){
...
 $form['interface_config']['driver_url'] = array(
        '#title' => t('Driver Url'),
        '#description' => t('url of the driver'),
        '#type' => 'textfield',
        '#default_value' => variable_get('kapasoft_driver_url', 'localhost'),
        '#required' => TRUE,
    );

    $form['interface_config']['driver_port'] = array(
        '#title' => t('Driver Port'),
        '#description' => t('port of the driver'),
        '#type' => 'textfield',
        '#default_value' => variable_get('kapasoft_driver_port', 3000),
        '#required' => TRUE,
    );
}

In the highlighted lines, the default value is retrieved from variable. If the variables hasn’t been set,yet, then it sets to the default values as specified(i.e. ‘locahlost’, 3000))

At submission, lets save the new variables as well:

function kapasoft_config_form_submit($form, &$form_state){
    //rebuild the form
    $form_state['rebuild'] = TRUE;

    //Save kapasoft setting variables
    variable_set('kapasoft_driver_url', $form_state['values']['driver_url']);
    variable_set('kapasoft_driver_port', $form_state['values']['driver_port']);
 
    //notify user
    drupal_set_message(t('kapasoft driver settings are saved'));
}

At form submission, the values from the form is taken and saved using the variable_set API

Variable_get With Defaults

So far as implemented, it only works with the form. If the form is not submitted, variables are not saved.

In addition, defaults are set in dozen places that don’t scale. In another words, we would have to edit code for each instance of variable_get/variable_set if the default value changes

So, we going to use hook_install to set the default variables once at the time module is installed. This hook is placed in moduleName.install file as following:

function kapasoft_config_install(){
    //set default variables
    variable_set('kapasoft_driver_url', 'localhost');
    variable_set('kapasoft_driver_port', '3000');

    //get localization function for installation as t() may not be available
    $t = get_t();

    //give user feedback
    drupal_set_message($t('Kapasoft driver configurations created'));
}

function kapasoft_config_uninstall(){
    //Delete variables
    variable_del('kapasoft_driver_url');
    variable_del('kapasoft_driver_url');
    
    //get localization function for installation as t() may not be available
    $t = get_t();
    
    //give user feedback
    drupal_set_message($t('Kapasoft driver configurations removed'));    
}

Next, remove all default values from moduleName.module file, so there is single point of initialization for defaults in the moduleName.install file(i.e. make variable_get(‘kapasoft_driver_url’, 3000) to variable_get(‘kapasoft_driver_url’))

Adding Configuration Button

To add a configuration button next to module name in the modules list, add the following line in the modulesName.info file:

...
configure = admin/config/kapasoft/manage

Here the path ‘admin/config/kapasoft/manage’ is the path specified in the moduleName_config_menu() function above

Shorten Code

We can shorten code by completely removing submit function(i.e kapasoft_config_settings_form_submit()) and let the Drupal built in mechanism handle our form submission by updating the form build function as follows:

function kapasoft_config_form($nodes, &$form_state){
...
//    $form['submit'] = array(
//        '#type' => 'submit',
//        '#value' => t('Save'),
//    );

    return system_settings_form($form);
}

As you see, we uncomment the submit button and return Drupal function ‘system_settings_form() with an argument that contains our admin form. This will handle the submission, save the variables and display the message to user. Note, make sure the variable names are the same as specified in form.

Load Other Modules

To utilize other modules within your module, all you need is to load it as following:

module_load_include('inc', 'location', 'earth');
module_load_include('inc', 'gmap3_tools');

This loads the ‘location’ and gmap3_tools modules. Afterwards, you are able to use any of their modules API

How To add Content Type

Here is a great article Creating Custom Content Type…

How to add Block

see post Blocks in Drupal

Troubleshooting

1. Form structure and values
When implementing form validation, it may be useful to print out the form structure

function bazar_node_form_validate($form, &$form_state){
    //to see what validated - the structure
    dpm($form_state);
    //purposely set error
    form_set_error('','testing');

The ‘bazar_node_form_validation’ is custom form validation function added to the list of function names that are used to validate the form in hook_form_alter:

/**
 * implements hook_form_alter()
 */
function bazar_form_alter(&$form, &$form_state, $form_id){
    //like to add validation but i don't know the form id
    dpm($form_id);
    //i would like to see the structure of the form
    dpm($form);
    //return to browser, clear craches and go to form

    //Afterwards, we can turn on debuging only for our form of interest. From the form id we can figure out the validation callback function to be bazar_node_form_validate
    switch($form_id){
        case 'bazar_node_form':{
            //dpm($form);
            //look for #validate that takes list of function names that are used to validate the form. I can ad custom funciton to this array
            $form['#validate'][] = 'bazar_node_form_validate';
            break;
        }
    }
}

Here, we are adding custom validation function ‘bazar_node_form_validate’ to the form with id ‘bazar_node_form’

2. Verify variables loaded
Here is an example to find out what variables are loaded for your theme function(the theme hook specifies theme funciotn)

function theme_ThemeName_gmap($variables){
    dpm($variables);
}

This will print out all the variables passed to this theme. The theme funciton ‘theme_ThemeName_gmap’ was specified in the theme_hook

Useful Links

  • https://api.drupal.org/api/drupal/