Hero

Como subir un archivo usando Form API en Drupal 7

Febrero 24, 2013

enzo

Aunque es posible procesar archivos usando el FAPI de Drupal 7. La documentación referente al file no es clara y se limita a informarnos que en el módulo file del core de Drupal encontraremos mas información, lo cual puede no ser muy educativo. Por tal motivo les explicare paso a paso como manejar archivos paso a paso.

Como extra bono se mostrara como crear usuarios de Drupal vía código utilizando los datos contenido en el archivo provisto por el usuario mediante el form.

  1. Definir página para el custom form.

Lo primero que debemos hacer es registrar un nuevo menu el cual llamara a el custom form que se encargara de subir y procesar un archivo, como se muestra a continuación, se asume que el custom módulo se llama MYMODULE.

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

 $items['admin/import-users'] = array(
 'title' => 'Import users to access books',
 'page callback' => 'drupal_get_form',
 'page arguments' => array('MYMODULE_import_users_form'),
 'access arguments' => array('access administration pages'),
 );

 return $items;
}

El custom form de ejemplo se usara para leer un archivo de CSV para importar nuevos usuarios a nuestro sistema Drupal.

  1. Crear custom form.

Como se indico en el menu, el menu llama a la función drupal_get_menu con el parámetro MYMODULE_import_users_form, la cual sera la función que declara el formulario como se puede observar en el siguiente listado.

/**
 * Menu callback: Generate a form to import users an associate to books
 *
 * @ingroup forms
 * @see MYMODULE_import_users_form_validate()
 * @see MYMODULE_import_users_form_submit()
 */
function MYMODULE_import_users_form($form, &$form_state, $arg = NULL) {

 $form['user_upload'] = array(
 '#type' => 'file',
 '#title' => t('Choose a file'),
 '#description' => t('File in CSV format to import users'),
 );

 $form['csv_delimiter'] = array(
 '#type' => 'select',
 '#title' => t('CSV Delimiter'),
 '#options' => array(',' => ",",';' => ';'),
 '#description' => t('Select what book will be associated to imported users'),
 ); 
 
 $form['actions'] = array('#type' => 'actions');
 $form['actions']['submit'] = array('#type' => 'submit',
 '#value' => t('import'),
 );

 return $form;
}

En anterior definición del formulario permite subir un archivo, ademas de un selector de delimitador del CSV. Para procesar el formulario siempre se debe agregar un elemento submit al que se ha llamado import.

El resultado sera similar al presentado en la siguiente imagen.

FAPI upload 0

Por si solo el formulario anterior no realiza ninguna opción, por lo tanto se deben implementar las funciones de validate y submit referenciadas en la documentación de la función, siempre es buena idea agregar esta información para poder guiarnos dentro del archivo de nuestro módulo customizado.

Para implementar una función de validate solo se debe crear una nueva función con el mismo nombre de la función que genera el formulario agregando la palabra _validate como se muestra en el siguiente listado.

La función validate sera ejecutada luego de presionar el botón de importar.

/**
 * Validate MYMODULE_import_users_form submissions.
 */
function MYMODULE_import_users_form_validate($form, &$form_state) {

 $file = $form_state['values']['users'];
 $delimiter = $form_state['values']['csv_delimiter'];
 
 $validators = array('file_validate_extensions' => array('csv'));

 if ($user_file = file_save_upload('user_upload', $validators, '../assets/')) {
 // Without this fgetcsv() fails for Mac-created files
 ini_set('auto_detect_line_endings', TRUE);

 $errors = array();
 $data= array();
 if ($fp = fopen($users_file->uri, 'r')) {
 while (!feof($fp)) {
 $row = fgetcsv($fp, NULL,$delimiter);
 $args = array(':mail' => db_like($row['0']));
 $uid = db_query_range('SELECT uid FROM {users} WHERE mail LIKE :mail', 0, 1, $args)->fetchField();
 
 if($uid) {
 $errors[] = t('User with email') . ' ' . $row[0] . ' ' . t('already exist'); 
 } else {
 $data['users'][] = $row;
 }
 
 }
 
 if(!empty($errors)) {
 form_set_error('user_upload', '<ul><li>' . implode('</li><li>', $errors) . '</li></ul>');
 return;
 }
 }
 
 // Save the validated data to avoid reparsing
 $form_state['storage']['data'] = $data;
 } else {
 form_set_error('user_upload', t('Cannot save the import file to temporary storage. Please try again.'));
 return;
 }
}

Veamos la implementación poco a poco.

2.1 Obtener valores

Primero se obtienes los valores del formulario para el archivo y el delimitador del CSV escogidos.

 $file = $form_state['values']['users'];
 $delimiter = $form_state['values']['csv_delimiter'];

2.2 Procesar archivo

Aunque pueda parecer extraño haremos el procesamiento del archivo en la validación, de modo que si la validación del archivo esta bien, pasaremos inmediatamente al submit.

En el siguiente listado se valida si el archivo enviado tiene la extensión CSV.

 $validators = array('file_validate_extensions' => array('csv'));

 if ($users_file = file_save_upload('user_upload', $validators, '../assets/')) {

La validación de la extensión se hace con la función file_save_upload la cual internamente llamara a la función file_validate_extensions con el parámetro csv y si todo esta bien guardara el archivo en la ubicación ../assets/ el cual es un stream wrapper definido por Drupal.

Si todo sale bien una referencia al archivo quedara en la variable $users_file y si falla retorna FALSE y se debe proceder a retornar un error.

2.3 Manejo de errores.

Si se ha detectado algún error, debemos reportarlo al usuario y eso lo haremos implementando la función form_set_error. La función form_set_error recibe 2 parámetros, el primero es el ID del elemento del FORM en marcara el error y el segundo parámetro es un texto que indicara el tipo de error.

Se usa la función t para garantizar que nuestro módulo sera traducible a múltiples idiomas, como se muestra en el siguiente fragmento de código.

form_set_error('user_upload', t('Cannot save the import file to temporary storage. Please try again.'));
return;

2.4 Validación de usuarios del CSV.

Luego de que el archivo se haya cargado correctamente, se debe validar que ninguno de los usuarios del CSV exista previamente en nuestro sistema Drupal.

Para lo cual haremos uso de las funciones estándar de PHP fopen y fgetcsv y se asumirá que el CSV tendrá los campos en este orden EMAIL,NOMBRES,APELLIDOS.

Para finalizar haremos una consulta a la base de datos para verificar que no existe algún usuario con email provisto en el archivo CSV usando la función db_query_range.

A continuación la implementación de la validación del archivo.

 // Without this fgetcsv() fails for Mac-created files
 ini_set('auto_detect_line_endings', TRUE);

 $errors = array();
 $data= array();
 if ($fp = fopen($user_file->uri, 'r')) {
 while (!feof($fp)) {
 $row = fgetcsv($fp, NULL,$delimiter);
 $args = array(':mail' => db_like($row['0']));
 $uid = db_query_range('SELECT uid FROM {users} WHERE mail LIKE :mail', 0, 1, $args)->fetchField();
 
 if($uid) {
 $errors[] = t('User with email') . ' ' . $row[0] . ' ' . t('already exist'); 
 } else {
 $data['users'][] = $row;
 }
 
 }

En caso de que algún usuario existe, se agregara al array $errors y si al finalizar de procesar el archivos existes errores se reportaran al usuario final como se muestra en el siguiente listado.

 if(!empty($errors)) {
 form_set_error('user_upload', '<ul><li>' . implode('</li><li>', $errors) . '</li></ul>');
 return;
 }

2.5 Salvar usuarios en el $form_state.

Ya que hemos procesado todo el archivo a importar en el proceso de validación no tiene sentido volver a repetir este proceso en la función submit del formulario. Para guardar los datos de los usuarios a importar se hará uso de la variable $form_state la cual es una variable por referencia que también es usada en el proceso de submit.

A continuación se puede observar como se guarda la variable $data creada en el proceso de validación de los usuarios.

 // Save the validated data to avoid reparsing
 $form_state['storage']['data'] = $data;
  1. Importar los usuarios.

En caso de que la función validación no haya presentado errores se procederá a ejecutar la función submit, para lo cual solo debemos crear una nueva función con el mismo nombre de la función que genero el formulario agregando la palabra ”_submit” que en nuestro caso seria MYMODULE_import_users_form_submit.

En el siguiente listado se puede observar el listado completo de la implementación del submit.

/**
 * Process MYMODULE_import_users_form submissions.
 */
function MYMODULE_import_users_form_submit($form, &$form_state) {
 $users = $form_state['storage']['data']['users'];
 $file = file_load($book);
 //Create users
 foreach ($users as $user) {
 $password = user_password(8);
 //set up the user fields
 $fields = array(
 'name' => $user[1] . ' ' . $user[2],
 'mail' => $user[0],
 'pass' => $password,
 'status' => 1,
 'init' => 'email address',
 'roles' => array(
 DRUPAL_AUTHENTICATED_RID => 'authenticated user', 
 ),
 );

 //the first parameter is left blank so a new user is created
 $account = user_save('', $fields);

 // If you want to send the welcome email, use the following code
 // Manually set the password so it appears in the e-mail.
 $account->password = $fields['pass'];
 //End create user

 // Send the e-mail through the user module.
 drupal_mail('user', 'register_no_approval_required', $user[0], NULL, array('account' => $account), variable_get('site_mail', 'noreply@example..com'));
 }

En la función anterior recorremos todos los valores contenido en la variable $form_state[‘storage’][‘data’][‘users’] la cual fue creada en la función validate. Se hace uso de la función user_save para crear los nuevos usuarios y de la función drupal_mail para llamar a la implementación del hook_mail del módulo user con la llave ”register_no_approval_required” para avisar al usuario que una nueva cuenta fue creada para el.

Con esto estaría completa nuestra implementación de un formulario que carga un archivo y se procese.

Recibe consejos y oportunidades de trabajo 100% remotas y en dólares de weKnow Inc.