Como importar un CSV con validación y feedback en PHP en WordPress

Contents

Introducción

Este artículo presenta un tutorial detallado para WordPress que explica cómo importar un archivo CSV con validación y feedback en PHP. Se describe un flujo seguro y práctico, con código listo para usar dentro de un plugin mínimo que añade una página en el área de administración. El objetivo es validar cada fila del CSV, importar solamente las filas válidas y proporcionar retroalimentación clara de los errores para poder corregir los datos y volver a intentar la importación.

Requisitos previos

  • Instalación de WordPress con permisos de administrador.
  • Conocimientos básicos de PHP y del API de WordPress (acciones, capacidades, inserción de posts).
  • Archivo CSV con codificación UTF-8 y separador por comas (o ajustar el separador en el código si fuera necesario).
  • Acceso al directorio de plugins para colocar el archivo PHP del plugin o conocimiento básico para crear un plugin.

Diseño general del proceso

  1. Formulario en el admin que permite subir el CSV y contiene un nonce de seguridad.
  2. Procesamiento en un handler (admin_post) que valida el nonce y la capacidad del usuario.
  3. Subida temporal del archivo con wp_handle_upload para mayor seguridad.
  4. Lectura del CSV con fgetcsv y validación por fila: campos obligatorios, formatos (email, numérico, fecha), unicidad, etc.
  5. Inserción de los registros válidos (ej. wp_insert_post) y recopilación de errores por fila para feedback.
  6. Almacenamiento del resultado (por ejemplo en transient) y redirección a la página de administración con una clave para mostrar los detalles del proceso.

Consideraciones de seguridad

  • Usar wp_nonce_field y check_admin_referer para prevenir CSRF.
  • Comprobar current_user_can(manage_options) para limitar la acción a administradores u otro rol apropiado.
  • Usar wp_handle_upload con test_form => false cuando se trata de subidas desde admin_post y validar el resultado.
  • Sanitizar y validar todos los campos antes de usarlos en la base de datos (esc_html, sanitize_email, intval, etc.).
  • No confiar en los datos del CSV: validación exhaustiva y manejo de errores robusto.

Ejemplo práctico: plugin mínimo para importar CSV con validación y feedback

A continuación se muestra un plugin completo (archivo único) que añade una página bajo Herramientas → Importar CSV. El plugin procesa el CSV, valida cada fila y guarda los resultados en un transient para mostrarlos después en la misma pantalla.

Código del plugin (guardar como, por ejemplo, csv-importer-validacion.php en la carpeta de plugins)

Resultados no disponibles o caducados.

} else { echo

Resumen de la importación

echo echo echo echo echo echo echo
Total filasImportadasErrores
. intval(results[total]) . . intval(results[imported]) . . intval(count(results[errors])) .
if (!empty(results[errors])) { echo

Errores por fila

echo foreach (results[errors] as err) { echo echo echo echo

echo

}
echo

FilaMotivoDatos
. intval(err[row]) . . esc_html(err[message]) .
 . esc_html(err[data]) . 

}

// Borrar transient al mostrar (opcional)
delete_transient(key)
}
}

// Formulario de subida
?>

Subir archivo CSV

enctype=multipart/form-data>


csv-import, result_key => ), admin_url(tools.php))
wp_safe_redirect(redirect)
exit
}

require_once ABSPATH . wp-admin/includes/file.php
overrides = array(test_form => false)
uploaded = wp_handle_upload(_FILES[csv_file], overrides)

if (empty(uploaded) !empty(uploaded[error])) {
redirect = add_query_arg(array(page => csv-import, result_key => ), admin_url(tools.php))
wp_safe_redirect(redirect)
exit
}

file = uploaded[file]
handle = fopen(file, r)

if (handle === false) {
redirect = add_query_arg(array(page => csv-import, result_key => ), admin_url(tools.php))
wp_safe_redirect(redirect)
exit
}

// Definir cabeceras esperadas
expected_headers = array(title, email, price, date)
row_num = 0
total = 0
imported = 0
errors = array()

// Leer cabecera del CSV
headers = fgetcsv(handle)
row_num

if (headers === false) {
fclose(handle)
redirect = add_query_arg(array(page => csv-import, result_key => ), admin_url(tools.php))
wp_safe_redirect(redirect)
exit
}

// Normalizar encabezados (trim y lower)
norm_headers = array_map(function(h){ return strtolower(trim(h)) }, headers)

// Comprobar que contiene los campos obligatorios
foreach (expected_headers as h) {
if (!in_array(h, norm_headers)) {
fclose(handle)
errors[] = array(row => 0, message => Falta columna obligatoria: {h}, data => implode(,, headers))
results = array(total => 0, imported => 0, errors => errors)
key = csv_import_ . time()
set_transient(key, results, 600)
redirect = add_query_arg(array(page => csv-import, result_key => key), admin_url(tools.php))
wp_safe_redirect(redirect)
exit
}
}

// Mapear posición de cada columna esperada
map = array()
foreach (norm_headers as idx => h) {
map[h] = idx
}

// Procesar cada fila
while ((data = fgetcsv(handle)) !== false) {
row_num
total

// Saltar filas vacías
is_empty = true
foreach (data as c) {
if (trim(c) !== ) { is_empty = false break }
}
if (is_empty) continue

// Recuperar valores por nombre
title = isset(data[map[title]]) ? trim(data[map[title]]) :
email = isset(data[map[email]]) ? trim(data[map[email]]) :
price = isset(data[map[price]]) ? trim(data[map[price]]) :
date = isset(data[map[date]]) ? trim(data[map[date]]) :

// Validaciones
row_errors = array()
if (title === ) {
row_errors[] = Título vacío
} else {
// Comprobar unicidad del título (opcional)
if (get_page_by_title(title, OBJECT, post)) {
row_errors[] = Ya existe un post con ese título
}
}

if (email === !filter_var(email, FILTER_VALIDATE_EMAIL)) {
row_errors[] = Email inválido
}

// Price debe ser numérico y mayor o igual a 0
price_clean = str_replace(,, ., price)
if (price_clean === !is_numeric(price_clean)) {
row_errors[] = Precio inválido
} else {
price_val = floatval(price_clean)
if (price_val < 0) row_errors[] = Precio negativo } // Fecha en formato YYYY-MM-DD dt = DateTime::createFromFormat(Y-m-d, date) if (date === dt === false dt->format(Y-m-d) !== date) {
row_errors[] = Fecha inválida (esperado YYYY-MM-DD)
}

if (!empty(row_errors)) {
errors[] = array(
row => row_num,
message => implode( , row_errors),
data => implode(, , data)
)
continue
}

// Si todo OK: insertar post (ejemplo) y meta con precio y email
postarr = array(
post_title => wp_strip_all_tags(title),
post_content => ,
post_status => publish,
post_type => post,
)

post_id = wp_insert_post(postarr, true)
if (is_wp_error(post_id)) {
errors[] = array(row => row_num, message => Error al crear el post: . post_id->get_error_message(), data => implode(, , data))
continue
}

// Guardar meta
update_post_meta(post_id, import_email, sanitize_email(email))
update_post_meta(post_id, import_price, floatval(price_val))
update_post_meta(post_id, import_date, dt->format(Y-m-d))

imported
}

fclose(handle)

// Preparar resultados y guardarlos en transient para mostrarlos en la página
results = array(
total => total,
imported => imported,
errors => errors
)

key = csv_import_ . time()
set_transient(key, results, 600)

// Redirigir de vuelta a la página del plugin con la clave de resultados
redirect = add_query_arg(array(page => csv-import, result_key => key), admin_url(tools.php))
wp_safe_redirect(redirect)
exit
}

Explicación del código

Formato de CSV de ejemplo

Cabecera esperada (orden no necesario, pero los nombres deben existir): title,email,price,date

title,email,price,date
Producto 1,usuario1@ejemplo.com,29.99,2025-01-15
Producto 2,usuario2@ejemplo.com,9.50,2025-02-01
Producto 3,invalid-email,15.00,2025-02-30

En el ejemplo anterior, la tercera fila tendría errores: email inválido y fecha inexistente (2025-02-30).

Buenas prácticas y mejoras recomendadas

  1. Adaptar las validaciones a los requerimientos reales (por ejemplo, comprobar relaciones con taxonomías, talles, stock, etc.).
  2. Soportar archivos grandes mediante procesamiento por lotes y uso de WP Cron o un sistema de colas para evitar timeouts.
  3. Añadir una vista previa (preview) antes de insertar para que el administrador confirme los cambios.
  4. Proporcionar un CSV de muestra descargable para que los usuarios sepan el formato exacto.
  5. Registrar logs de importación y permitir reintentos o correcciones desde la interfaz.
  6. Si se espera subir muchos archivos o archivos muy grandes, limitar tipos MIME permitidos y revisar límites de PHP (upload_max_filesize, post_max_size).

Conclusión

Con este enfoque se obtiene una importación segura y controlada: el usuario recibe feedback preciso sobre qué filas fallaron y por qué, y sólo se importan los registros que superan las validaciones. El ejemplo de plugin sirve como base que se puede extender para soportar estructuras de datos más complejas, custom post types, taxonomías y procesos por lotes para grandes volúmenes.

Enlaces útiles



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *