background image

Content tagged with: api

Eric's picture

In this code snippet, I'll show how you can parse a (large) CSV file using Drupal's Batch API. The purpose of batching an operation is to avoid PHP memory limits and time outs. Before you begin, I recommend reviewing the following two articles. Be sure to review the additional batch parameters outlined in the documentation, you might need to use them.

Batch API
Batch Operations

We can start by defining the arguments that will be passed into the batch_set() function. For this example, I added this code in an arbitrary page callback function.

<?php
function MYMODULE_callback_csv_import() {

 
// define path to CSV file
 
$csv_file_path = file_directory_path() . '/import_path/myfile.csv';

 
// define a redirect path upon batch completion
 
$redirect_path = 'admin/import-csv';

 
// define batch array structure
  // NOTE: minimal parameters defined to simplify code
 
$batch = array(
   
'title' => t('Reading File'),
   
'operations' => array(
      array(
       
'_MYMODULE_batch_read', array($csv_file_path),
      ),
    ),
  );

 
// set batch
 
batch_set($batch);

 
// process batch
 
batch_process($redirect_path);

}
?>

Next, we'll define the batch callback function. This function will be called repeatedly until the $context['finished'] variable is set to "1".

<?php
function _MYMODULE_batch_read($csv_file_path, &$context) {
 
 
// define batch limit
 
$batch_limit = 100;

 
// assume the batch process has not completed
 
$context['finished'] = 0;

 
// open the file for reading
 
$file_handle = fopen($csv_file_path, 'r');

 
// check if file pointer position exists in the sandbox, and jump to location in file
 
if ($context['sandbox']['file_pointer_position']) {
   
fseek($file_handle, $context['sandbox']['file_pointer_position']);
  }
 
 
// loop through the file and stop at batch limit
 
for ($i = 0; $i < $batch_limit; $i++) {

   
// get file line as csv
   
$csv_line = fgetcsv($file_handle);

   
// NOTE: at this point, do what ever you'd like with the CSV array data!
   
if (is_array($csv_line)) {
     
// db_query(), etc   
   
}

   
// retain current file pointer position
   
$context['sandbox']['file_pointer_position'] = ftell($file_handle);

   
// check for EOF
   
if (feof($file_handle)) {
     
// complete the batch process
     
$context['finished'] = 1;

     
// end loop
     
break;
    }

  }

}
?>

The batch operation will be called until the end of the CSV file is reached. The $context variable is passed by reference into the batch callback so you can maintain data through each iteration; in this case, the position of the file pointer. When the batch operation is complete, the user will be redirected to the batch_process() path argument.

It's important to read the full Batch API documentation so you can take advantage of its additional features: finished callback, init_message, progress_message, error_message, etc.

Eric's picture

When I'm coding modules, I rely heavily on the information found here: http://api.drupal.org/api/functions. Searching for functions can show you what already exists and how to use them in your code. Before you code something from scratch (or start to write your own SQL statement), you should start with the API. To be honest, I can't say that I've never coding something from scratch.

Another way to figure out how Drupal functionality is generated is to check out the menu hook in the module. A lot of helpful information resides here. For instance, you might want to embed a forum in a block or a custom module. If the functionality you're trying to recreate is found on your site at http://YOURSITE/forum, look for the menu_hook entry for "forum". Open /modules/forum/forum.module, locate the forum_menu() function, and look in the associative array for the "forum" entry:

// it starts with:
$items['forum'] = array(

In this case, the "page callback" property of the array tells you which callback function is being used. If you search the forum.module file, you notice that this function does not exist. The "file" property of the array tells you that this function requires an additional file to function properly, in this case "forum.pages.inc". At this point, it's helpful to review the code in the page callback function and use the online API documentation as a reference. NOTE: more information about hook_menu() can be found here: http://api.drupal.org/api/function/hook_menu/6.

For this example, I decided to use the forum_get_forum() function in my code. Before I can simply call this function, I must include the same file defined in the "file" property of the menu_hook entry:

<?php
require_once(drupal_get_path('module','forum') . '/forum.pages.inc');
$forums = forum_get_forums(0);
?>

Now, I have the html generated from the forum module. You could use this same process to recreate functionality from other modules as well.

Eric's picture

Although I am a big fan of the Drupal Forms API, sometimes I'll use CCK node forms as a quick and flexible alternative. This allows you to store the user submitted data as well as process the form. Here is a code snippet you can add to send the html generated from the node in an HTML email...

<?php
function MYMODULE_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  if (
$node->type == 'MYNODETYPE' && $op=='insert') {
   
$emailTo = variable_get('site_mail', 'MYEMAILADDRESS');
   
$emailFrom = 'info@' . $_SERVER['HTTP_HOST'];
       
   
// send email
   
_MYMODULE_send_node_email($node, $emailTo, $emailFrom);
       
  }
   
}

function
_MYMODULE_send_node_email(&$node, $emailTo, $emailFrom) {

  require_once(
'Mail.php');
  require_once(
'Mail/mime.php');

 
// create node html
 
$html = node_view($node, TRUE, FALSE, FALSE);
   
 
// get a list of node types
 
$nodeTypes = node_get_types();
   
 
$crlf = "\n";
 
$hdrs = array(
   
'From'    => $emailFrom,
   
'Subject' => $nodeTypes[$node->type]->name . ' Submitted',
  );
   
 
$mime = new Mail_mime($crlf);
 
//$mime->setTXTBody($text);  //TODO: you should also include a text version of your email
 
$mime->setHTMLBody($html);
 
$body = $mime->get();
 
$hdrs = $mime->headers($hdrs);
 
$mail =& Mail::factory('mail');
 
$mail->send($emailTo, $hdrs, $body);  

}
?>

Eric's picture

Here is the code I used to add a password strength meter to the user edit form.

First, I added a menu item that will be used by AJAX to process the user's password. The path will be used like this: http://MYWEBSITE/passwordStrength/USERPASSWORD. As you can see in the callback arguments, the remainder of the query string after the first "/" is passed into the callback function.

<?php
function MYMODULE_menu() {
 
$items = array();
 
// ...code...
 
$items[] = array(
   
'path' => 'passwordStrength',
   
'callback' => 'MYMODULE_passwordStrength',
   
'type' => MENU_CALLBACK,
   
'access' => TRUE,
   
'callback arguments' => array(array_pop(explode("/",$_REQUEST['q'],2))),
  );
 
// ...code...
 
return $items;
}
?>

Here's the callback function. It returns an integer to the screen with no theming.

<?php
function patient_portal_callback_passwordStrength($password) {
   
 
// NOTE: "SSL: Fatal Protocol Error" occurring
  // echo file_get_contents('https://www.google.com/accounts/RatePassword?Passwd=' . $password);
   
  // NOTE: google will return 1-4

  // check for invalid password
 
if (strlen($password)==0 || !$password) {
    echo
1;
    die;   
  }
   
 
// using CURL to fetch website result
 
$ch = curl_init();
 
curl_setopt($ch, CURLOPT_URL, 'https://www.google.com/accounts/RatePassword?Passwd=' . urlencode($password));
 
curl_exec($ch);   
 
curl_closE($ch);
   
  die;

}
?>

The next part is modifying the user edit form using the form_alter hook.

<?php
function MYMODULE_form_alter($form_id, &$form) {
 
// ...code...
 
if ($form_id == 'user_edit') {

   
// add weights to current account form elements
   
if (isset($form['account']['name'])) $form['account']['name']['#weight'] = 1;
    if (isset(
$form['account']['mail'])) $form['account']['mail']['#weight'] = 2;
    if (isset(
$form['account']['pass'])) $form['account']['pass']['#weight'] = 3;
    if (isset(
$form['account']['status'])) $form['account']['status']['#weight'] = 5;
    if (isset(
$form['account']['roles'])) $form['account']['roles']['#weight'] = 6;
       
   
// create html for password meter
   
$passwordMeterHtml = "
      <div id='passwordMeterFormItem' class='form-item'>
        <label>
          <span id='passwordStrengthLabel'>Password Strength:</span>
          <span id='passwordStrengthDescription'></span>
        </label>
        <table id='passwordMeter'>
          <tr>
            <td id='barLeft'></td>
            <td id='barRight'></td>
          </tr>
        </table>
      </div>
    "
;
       
   
// add form "element"
   
$form['account']['passwordMeter'] = array(
     
'#value' => $passwordMeterHtml,
     
'#weight' => 4,
    );
       
   
// add js to password input
   
$js = "
      $(document).ready(function(){
        $('form#user-edit #edit-pass-pass1').keyup(function(e){
                   
          if (this.value.length) {
            $.ajax({
              type: 'GET',
              url: '/passwordStrength/' + this.value,
              success: function(passwordCode){
                switch (passwordCode) {
                  case '1':
                    word = 'Weak';
                    break;
                  case '2':
                    word = 'Fair';
                    break;
                  case '3':
                    word = 'Good';
                    break;
                  case '4':
                    word = 'Strong';
                    break;
                }
   
                // remove td classes
                $('table#passwordMeter td#barLeft').removeClass();
                $('table#passwordMeter td#barRight').removeClass();
                               
                // add td classes
                $('table#passwordMeter td#barLeft').addClass(word);
                $('table#passwordMeter td#barRight').addClass(word);

                // set description
                $('span#passwordStrengthDescription').html(word);
                $('span#passwordStrengthDescription').removeClass();
                $('span#passwordStrengthDescription').addClass(word);
              }
            });                   
          } else {
            // remove td classes
            $('table#passwordMeter td#barLeft').removeClass();
            $('table#passwordMeter td#barRight').removeClass();
                       
            // set description
            $('span#passwordStrengthDescription').html('');
            $('span#passwordStrengthDescription').removeClass();
          }
        });
      });
    "
;
   
drupal_add_js($js, 'inline');

  }
 
// ...code...
}
?>

Lastly, I added some CSS

div#passwordMeterFormItem {
  width: 210px;
}

table#passwordMeter {
  width: 100%;
  height: 10px;
  margin: 0;
  clear: both;
}

span#passwordStrengthLabel {
  float: left;
}

table#passwordMeter tbody, table#passwordMeter tr {
  border: none;
}

table#passwordMeter td {
  padding: 0;
  height: 10px;
}

table#passwordMeter td#barLeft {
  background-color: #e0e0e0;
  width: 0%;
}

table#passwordMeter td#barRight {
  background-color: #e0e0e0;
  width: 100%;
}

table#passwordMeter td#barLeft.Weak {
  width: 25%;
  background-color: #AA0033;
}

table#passwordMeter td#barRight.Weak {
  width: 75%;
}

table#passwordMeter td#barLeft.Fair {
  width: 50%;
  background-color: #FFCC33;
}

table#passwordMeter td#barRight.Fair {
  width: 50%;
}

table#passwordMeter td#barLeft.Good {
  width: 75%;
  background-color: #6699CC;
}

table#passwordMeter td#barRight.Good {
  width: 25%;
}

table#passwordMeter td#barLeft.Strong {
  width: 100%;
  background-color: #008000;
}

table#passwordMeter td#barRight.Strong {
  width: 0%;
}

span#passwordStrengthDescription {
  display: block;
  float: right;
}

span#passwordStrengthDescription.Weak {
  color: #AA0033;
}

span#passwordStrengthDescription.Fair {
  color: #FFCC33;
}

span#passwordStrengthDescription.Good {
  color: #6699CC;
}

span#passwordStrengthDescription.Strong {
  color: #008000;
}

Here's a screen shot...