Mini aplicación express en PHP

Un día me encargaron hacer un aplicación de manera urgente, esto es, si la podía hacer en tres horas mejor que en cuatro, el planteamiento de la misma era el siguiente:

Necesitamos una manera de que varios candidatos rellenen un cuestionario y nos lo devuelvan, pero además sólo tienen una hora para rellenar el cuestionario.

Me entregaron un documento de Word con un montón de preguntas y me lanzaron al ruedo, en este post voy a explicar lo que hice.

Sabía que al tener el tiempo tan justo iba a tener que hacer concesiones, tanto en validaciones y buenas prácticas como en posibles bugs de la aplicación, pero me puse a ello. Una ventaja es que ya quedó claro que no iba a ser un código que hubiera que mantener, así que con que funcionara esta vez para este propósito valdría.

El primer paso es plantear la aplicación, mi primer impulso fue plantear un formulario con las preguntas del cuestionario y controlar el tiempo de acceso al mismo mediante javascript (con respaldo del backend del servidor) y guardar los resultados en una tabla (u otro medio). Pensaba haber cogido todas las respuestas del cuestionario, crear un objeto json y almacenarlo y luego ya pensaría como procesar los datos. Pero descarté esa posibilidad, en cuanto me puse a plasmarla en papel me di cuenta de que iba a acabar haciendo algo demasiado complejo para lo que necesitaba y aunque me parecía (y me lo sigue pareciendo) una buena manera de hacerlo, no iba a ser capaz de tener algo estable y sólido en tan poco tiempo.

Así que opté por algo más tradicional, HTML, PHP y una tabla MySQL. El funcionamiento de la aplicación sería el siguiente: Una vez «logueados», los candidatos tendrían acceso a la descarga del cuestionario en Word, en ese momento se quedaría registrado el tiempo en la base de datos, una vez completado el cuestionario los candidatos tendrían que subir el cuestionario y se quedaría registrado el tiempo de entrega. Para el análisis del cuestionario simplemente entregaría una hoja con el email del participante, el nombre del archivo asociado a su usuario y el tiempo empleado para rellenarlo; además de los propios cuestionarios, claro.

La aplicación así planteada serían tres pantallas: una de bienvenida con las instrucciones y un formulario para introducir el email (es el campo identificador), una con la descarga del cuestionario y el formulario para subirlo, y una de agradecimiento por la participación. Además de los procesos correspondientes, claro. Lo plasmo en papel: dibujo las pantallas, establezco la lógica y apunto los posibles puntos de fallos que se me ocurren, por ejemplo: ¿qué pasa si el candidato cierra la ventana del navegador? ¿y si recarga la página?

Vale, lo primero es decidir que herramientas voy a utilizar: Slim para las rutas, NotORM para las consultas (no lo había utilizado nunca, pero al ser algo sencillo me serviría para probarlo) y Upload para manejar las subidas del cuestionario. Con esto mi composer.json quedaría así:

{
  "require": {
    "slim/slim": "2.*",
    "vrana/notorm": "dev-master",
    "codeguy/upload": "*"
  }
}

La estructura de directorios de la aplicación:

/app/files/ //Donde se guardarán los cuestionarios completados
/app/lib/functions.php //Para las funciones de logueo, grabación, ...
/app/sass/master.scss //Incluso para tan poco merece la pena usar un preprocesador, aunque sólo sea por si acaso
/app/templates/ //Las plantillas
/public/index.php //La lógica de la aplicación

Entonces el archivo de la aplicación sería:

<?php
// /public/index.php
// Voy a utilizar sesiones para comprobar que el usuario esté logueado, así que inicio aquí y ya me lo quito
session_start();
// Autoload de las librerías de composer
include '../vendor/autoload.php';
// Las funciones creadas para la aplicación
include '../app/lib/functions.php';

// Configuro Slim, simplemente le paso el directorio dónde están las plantillas, al ser tan pocas pantallas me sirve la gestión básica de vistas que viene incluida con Slim
$app = new \Slim\Slim(array(
  'templates.path' => '../app/templates'
));

// Pantalla de bienvenida, instrucciones y logueo
$app->get('/', function() use ($app) {
  $app->render('index.php');
});

// Ruta de destino del proceso del formulario de entrada
$app->post('/init', function() use ($app) {
  // Dato identificativo
  $email = $app->request->post('email');

  // Si no es un email devuelvo a la pantalla de inicio, no muestro ningún mensaje, primera concesión
  if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $app->redirect('/');
  }

  // check_mail devuelve los datos de la tabla relacionados con ese email, si el email no existe crea además un nuevo registro
  $application = check_email($email);
  // Si ya se ha registrado hora de entrega no se permite modificar ni volver a subir el archivo, poco elegante pero funciona
  if ($application['end_time'] !== '0000-00-00 00:00:00') {
    $app->redirect('/end');
  }
  // Guardo el id como registro para validar los datos de entrega
  $app->flash('id', $application['id']);
  // Y vamos la página de descarga/subida del cuestionario
  $app->redirect('/test');
});

// La página de descarga/subida del cuestionario
$app->get('/test', function() use ($app) {
  // Como guardo el valor del id para un campo oculto del formulario necesito que ese dato se mantenga entre peticiones (por ejemplo en el caso de que recarguen la página)
  $app->flashKeep();
  // Necesito recuperara el id
  $flash = $app->view()->getData('flash');
  $id = $flash['id'];
  // y en el caso de no tenerlo mandarlos a la pantalla de logueo para que pueda loguearse de nuevo y continuar el proceso de subida del cuestionario (lo explico en las instrucciones y así no tengo que pasar mensajes ni nada)
  if (empty($id)) {
    $app->redirect('/');
  }
  $app->render('test.php');
});

// Proceso de subida del cuestionario
$app->post('/end', function() use ($app) {
  // Recupero el id del candidato
  $id = $app->request->post('id');
  // Inicio la subida del archivo
  $storage = new \Upload\Storage\FileSystem('../app/files');
  $file = new \Upload\File('doc', $storage);
  // Cambio el nombre del archivo a un nombre único
  $new_filename = uniqid();
  $file->setName($new_filename);
  // Muevo el archivo a su carpeta final y si no hay errores, continúo
  try {
    $file->upload();
  } catch (\Exception $e) {
    $errors = $file->getErrors();
    $app->redirect('/test');
  }

  // Grabo en la base de datos el tiempo de entrega
  update_db($id);

  // Vamos a la pantalla de despedida/agradecimiento
  $app->redirect('/end');

});

// Pantalla de despedida/agradecimiento
$app->get('/end', function() use ($app) {
  $app->render('end.php');
});

$app->run();

Y las funciones utilizadas:

<?php
// /app/lib/functions.php

// Compruebo si el candidato tiene registro, si no tiene grabo uno nuevo y devuelvo los datos
function check_email($email) {
  if (!get_id($email)) {
    insert_into_db($email);
  }
  return get_id($email);
}

// Devuelve los datos del candidato (el id y el timestamp de entrega)
function get_id($email) {
  $db = get_connection();
  $applications = $db->applications('email = ?', trim($email))->fetch();

  if (!$applications) {
    return false;
  }

  return array(
    'id' => $applications['id'],
    'end_time' => $applications['end_time']
  );
}

// Grabo un nuevo registro con el email y el tiempo de inicio
function insert_into_db($email) {
  $db = get_connection();
  $db->applications()->insert(array(
    'email' => trim($email),
    'init_time' => new NotORM_Literal('NOW()')
  ));
  return true;
}

// Grabo el tiempo de entrega del cuestionario
function update_db($id) {
  $db = get_connection();

  $db->applications()->insert_update(
    array('id' => $id),
    array('end_time' => new NotORM_Literal('NOW()'))
  );

}

// Devuelvo una instancia de NotORM
function get_connection() {
  $pdo = new PDO('mysql:dbname=table_name', 'user', 'password');
  $db = new NotORM($pdo);
  return $db;
}

Las plantillas no tienen más misterio, simple HTML, en el caso del formulario del archivo subido añado un campo oculto con el id del participante para poder actualizar el registro.

El tiempo utilizado para realizar esto, fue más o menos así:

  • Planteamiento de la aplicación - 40 minutos
  • Preparación del entorno de desarrollo - 15 minutos
  • Desarrollo hasta primera prueba - 1 hora
  • Revisión y pruebas del sistema - 10 minutos
  • Modificaciones y solventar fallos - 50 minutos
  • Estilos de la aplicación - 10 minutos

Es alucinante el avance en la preparación y en el desarrollo de aplicaciones con PHP desde la aparición de composer, sobre todo para este caso de mini-aplicaciones.