Como crear un importador desde JSON con validación en PHP en WordPress

Contents

Introducción

En este artículo encontrarás un tutorial completo y detallado para crear un importador de contenido en WordPress que lea datos desde JSON y realice validaciones en PHP antes de insertar o actualizar contenido. Se incluye un plugin mínimo funcional, validaciones robustas, gestión de archivos remotos, manejo de imágenes, control de duplicados, registro de errores y buenas prácticas de seguridad y rendimiento.

Requisitos y consideraciones previas

  • WordPress: 5.0 (funciones como wp_insert_post, media_handle_sideload son estándar desde hace tiempo).
  • Permisos: usuario con capacidad manage_options (u otra capacidad adaptada a tu entorno).
  • PHP: 7.2 recomendado ajustar según entorno.
  • Tamaño y timeout: ten en cuenta límites de upload_max_filesize, post_max_size, max_execution_time para grandes importaciones considera WP-CLI o procesos por lotes.
  • Copia de seguridad: siempre realiza backup de la base de datos antes de ejecutar importaciones masivas.

Ejemplo de formato JSON esperado

Este importador espera un JSON con un array de objetos. Cada objeto representa un post a crear/actualizar. Ejemplo mínimo:

[
  {
    external_id: 12345,
    post_type: post,
    status: publish,
    title: Título de ejemplo,
    content: Contenido en HTML o texto plano,
    excerpt: Extracto corto,
    date: 2025-09-01 12:00:00,
    author_email: autor@ejemplo.com,
    terms: {
      category: [Noticias, Lanzamientos],
      post_tag: [lanzamiento, producto]
    },
    meta: {
      precio: 19.99,
      stock: 10
    },
    thumbnail: https://ejemplo.com/imagenes/cover.jpg
  }
]

Estructura del plugin

Un plugin simple con un único archivo puede ser suficiente para comenzar:

  • /wp-content/plugins/json-importer/json-importer.php

El plugin registrará una página de administración con un formulario para subir el archivo JSON o indicar una URL, luego procesará el JSON y mostrará un resumen.

Cabecera del plugin (archivo principal)

lt?php
/
Plugin Name: JSON Importer con Validación
Description: Importador de posts desde JSON con validación y manejo de imágenes.
Version: 1.0
Author: Tu Nombre
Text Domain: json-importer
/

if ( ! defined( ABSPATH ) ) {
    exit
}

class JSON_Importer {
    public function __construct() {
        add_action( admin_menu, array( this, add_admin_page ) )
        add_action( admin_post_json_importer_process, array( this, handle_import ) )
    }

    public function add_admin_page() {
        add_menu_page(
            JSON Importer,
            JSON Importer,
            manage_options,
            json-importer,
            array( this, render_admin_page ),
            dashicons-upload
        )
    }

    public function render_admin_page() {
        include plugin_dir_path( __FILE__ ) . templates/admin-page.php
    }

    public function handle_import() {
        // Implementación más abajo...
    }
}

new JSON_Importer()

Formulario de subida y opciones

El template admin-page.php mostrará un formulario con:

  • Campo para subir archivo JSON
  • Campo para URL remota
  • Checkbox para modo dry-run (simulación sin inserciones)
  • Nonce de seguridad
lt?php
// archivo: templates/admin-page.php
if ( ! current_user_can( manage_options ) ) {
    wp_die( No tienes permisos suficientes. )
}
?>
lth2gtImportar desde JSONlt/h2gt
ltform method=post action= enctype=multipart/form-datagt
    ltinput type=hidden name=action value=json_importer_processgt
    lt?php wp_nonce_field( json_importer_nonce, json_importer_nonce_field ) ?gt
    ltpgtltlabelgtArchivo JSON:ltinput type=file name=json_file accept=.jsongtlt/labelgtlt/pgt
    ltpgtltlabelgto URL pública:ltinput type=url name=json_url style=width:60%gtlt/labelgtlt/pgt
    ltpgtltlabelgtModo simulación (no inserta)ltinput type=checkbox name=dry_run value=1gtlt/labelgtlt/pgt
    ltpgtltinput type=submit class=button button-primary value=Procesar importacióngtlt/pgt
lt/formgt

Procesamiento y validación del JSON

En el método handle_import se deben realizar las siguientes acciones en orden:

  1. Comprobar nonce y capacidad del usuario.
  2. Obtener contenido del JSON (archivo subido o URL remota).
  3. Decodificar JSON con json_decode y comprobar errores.
  4. Validar que la estructura es la esperada (array de objetos) y cada campo requerido.
  5. Por cada registro, validar tipos (string, date, number), longitudes y patrones.
  6. Realizar la inserción/actualización en WordPress, con registro de errores y resultados.

Función de validación simple pero robusta

Se muestra una función de validación que compara un esquema básico y genera errores por registro.

protected function validate_item( item ) {
    errors = array()

    if ( ! is_array( item ) ) {
        errors[] = El ítem no es un objeto/array.
        return errors
    }

    // Campos requeridos
    required = array( external_id, post_type, title )
    foreach ( required as r ) {
        if ( empty( item[ r ] ) ) {
            errors[] = Falta el campo requerido: r
        }
    }

    // Validaciones de tipo
    if ( isset( item[date] ) ) {
        d = strtotime( item[date] )
        if ( d === false ) {
            errors[] = Formato de fecha inválido: {item[date]}
        }
    }

    if ( isset( item[thumbnail] )  ! filter_var( item[thumbnail], FILTER_VALIDATE_URL ) ) {
        errors[] = URL de thumbnail inválida: {item[thumbnail]}
    }

    if ( isset( item[meta] )  ! is_array( item[meta] ) ) {
        errors[] = El campo meta debe ser un objeto/array.
    }

    return errors
}

Ejemplo de manejo completo (procesamiento e inserción)

El siguiente fragmento ilustra el flujo: lectura, validación, creación/actualización y registro de resultados.

public function handle_import() {
    if ( ! current_user_can( manage_options ) ) {
        wp_die( Permisos insuficientes )
    }

    if ( ! isset( _POST[json_importer_nonce_field] )  ! wp_verify_nonce( _POST[json_importer_nonce_field], json_importer_nonce ) ) {
        wp_die( Nonce no válido )
    }

    dry_run = isset( _POST[dry_run] )  _POST[dry_run] == 1
    json_content = 

    // 1) Si se subió archivo
    if ( ! empty( _FILES[json_file][tmp_name] ) ) {
        tmp = _FILES[json_file][tmp_name]
        json_content = file_get_contents( tmp )
    } elseif ( ! empty( _POST[json_url] ) ) {
        response = wp_remote_get( esc_url_raw( _POST[json_url] ), array( timeout => 20 ) )
        if ( is_wp_error( response ) ) {
            wp_die( Error al descargar JSON:  . response->get_error_message() )
        }
        json_content = wp_remote_retrieve_body( response )
    } else {
        wp_die( No se proporcionó archivo ni URL. )
    }

    data = json_decode( json_content, true )
    if ( json_last_error() !== JSON_ERROR_NONE ) {
        wp_die( Error al decodificar JSON:  . json_last_error_msg() )
    }

    if ( ! is_array( data ) ) {
        wp_die( Formato JSON inválido: se esperaba un array de objetos. )
    }

    results = array()

    foreach ( data as index => item ) {
        item_errors = this->validate_item( item )

        if ( ! empty( item_errors ) ) {
            results[] = array( index => index, status => error, messages => item_errors )
            continue
        }

        // Chequear duplicados por external_id guardado en post_meta
        existing = null
        if ( ! empty( item[external_id] ) ) {
            posts = get_posts( array(
                post_type => item[post_type],
                meta_key  => import_external_id,
                meta_value=> item[external_id],
                numberposts=> 1,
                post_status => any
            ) )
            if ( ! empty( posts ) ) {
                existing = posts[0]
            }
        }

        post_data = array(
            post_title   => wp_strip_all_tags( item[title] ),
            post_content => isset( item[content] ) ? wp_kses_post( item[content] ) : ,
            post_excerpt => isset( item[excerpt] ) ? sanitize_text_field( item[excerpt] ) : ,
            post_status  => isset( item[status] ) ? sanitize_key( item[status] ) : draft,
            post_type    => isset( item[post_type] ) ? sanitize_key( item[post_type] ) : post,
        )

        if ( isset( item[date] ) ) {
            post_data[post_date] = date( Y-m-d H:i:s, strtotime( item[date] ) )
        }

        // Autor por email (si existe)
        if ( ! empty( item[author_email] ) ) {
            user = get_user_by( email, sanitize_email( item[author_email] ) )
            if ( user ) {
                post_data[post_author] = user->ID
            }
        }

        if ( dry_run ) {
            results[] = array( index => index, status => dry_run, post_data => post_data )
            continue
        }

        if ( existing ) {
            post_data[ID] = existing->ID
            post_id = wp_update_post( post_data, true )
            action = updated
        } else {
            post_id = wp_insert_post( post_data, true )
            action = inserted
        }

        if ( is_wp_error( post_id ) ) {
            results[] = array( index => index, status => error, messages => array( post_id->get_error_message() ) )
            continue
        }

        // Guardar external_id como meta para evitar duplicados futuros
        if ( ! empty( item[external_id] ) ) {
            update_post_meta( post_id, import_external_id, sanitize_text_field( item[external_id] ) )
        }

        // Terms (categorías y etiquetas)
        if ( isset( item[terms] )  is_array( item[terms] ) ) {
            foreach ( item[terms] as taxonomy => terms ) {
                if ( ! taxonomy_exists( taxonomy ) ) {
                    continue
                }
                if ( ! is_array( terms ) ) {
                    terms = array( terms )
                }
                clean_terms = array_map( sanitize_text_field, terms )
                wp_set_object_terms( post_id, clean_terms, taxonomy )
            }
        }

        // Meta fields
        if ( isset( item[meta] )  is_array( item[meta] ) ) {
            foreach ( item[meta] as meta_key => meta_value ) {
                update_post_meta( post_id, sanitize_key( meta_key ), maybe_serialize( meta_value ) )
            }
        }

        // Imagen destacada
        if ( ! empty( item[thumbnail] )  filter_var( item[thumbnail], FILTER_VALIDATE_URL ) ) {
            thumb_id = this->sideload_image_from_url( item[thumbnail], post_id )
            if ( is_wp_error( thumb_id ) ) {
                results[] = array( index => index, status => warning, messages => array( thumb_error => thumb_id->get_error_message() ) )
            } else {
                set_post_thumbnail( post_id, thumb_id )
            }
        }

        results[] = array( index => index, status => success, post_id => post_id, action => action )
    }

    // Mostrar resultados simples y volver a la página admin
    set_transient( json_importer_results_ . get_current_user_id(), results, 30 )
    wp_safe_redirect( admin_url( admin.php?page=json-importerimported=1 ) )
    exit
}

Función para importar la imagen (sideload)

protected function sideload_image_from_url( url, post_id = 0 ) {
    require_once( ABSPATH . wp-admin/includes/file.php )
    require_once( ABSPATH . wp-admin/includes/media.php )
    require_once( ABSPATH . wp-admin/includes/image.php )

    // Validación básica de URL y tipo
    filetype = wp_check_filetype( basename( url ) )
    if ( empty( filetype[ext] ) ) {
        return new WP_Error( invalid_filetype, Tipo de archivo no reconocido para la URL. )
    }

    // Descarga remota temporal
    tmp = download_url( url, 15 ) // 15s timeout
    if ( is_wp_error( tmp ) ) {
        return tmp
    }

    desc = basename( url )
    file = array(
        name     => sanitize_file_name( desc ),
        tmp_name => tmp
    )

    attach_id = media_handle_sideload( file, post_id )

    if ( is_wp_error( attach_id ) ) {
        @unlink( tmp )
        return attach_id
    }

    return attach_id
}

Manejo de resultados y visualización

En la página admin después de procesar, se pueden mostrar los resultados almacenados en un transient. Ejemplo simple para mostrar una tabla de resultados:

lt?php
// fragmento en templates/admin-page.php para mostrar resultados si existen
results = get_transient( json_importer_results_ . get_current_user_id() )
if ( results ) {
    delete_transient( json_importer_results_ . get_current_user_id() )
    echo lth3gtResultados de la importaciónlt/h3gt
    echo lttable class=widefatgtlttheadgtlttrgtltthgtÍndicelt/thgtltthgtEstadolt/thgtltthgtDetalleslt/thgtlt/trgtlt/theadgtlttbodygt
    foreach ( results as r ) {
        echo lttrgt
        echo lttdgt . esc_html( r[index] ) . lt/tdgt
        echo lttdgt . esc_html( r[status] ) . lt/tdgt
        echo lttdgt
        if ( isset( r[messages] )  is_array( r[messages] ) ) {
            echo ltulgt
            foreach ( r[messages] as m ) {
                echo ltligt . esc_html( print_r( m, true ) ) . lt/ligt
            }
            echo lt/ulgt
        } else {
            echo esc_html( print_r( r, true ) )
        }
        echo lt/tdgt
        echo lt/trgt
    }
    echo lt/tbodygtlt/tablegt
}
?gt

Buenas prácticas y recomendaciones

  • Modo dry-run: siempre prueba con algunos registros en modo simulación antes de insertar masivamente.
  • Backup: exporta la BD y los archivos antes de una importación a producción.
  • Lotes: para grandes volúmenes, procesa en lotes (por ejemplo 50-200 registros por request) y emplea cron o un handler que reciba offsets para no agotar memoria ni tiempo.
  • WP-CLI: para importaciones masivas, implementar un comando WP-CLI es más eficiente y evita límites de PHP-FPM/web.
  • Validación avanzada: si necesitas validación de esquema compleja, considera usar librerías de JSON Schema vía Composer (por ejemplo opis/json-schema) e incluirlas en tu plugin, o implementar validaciones específicas por campo como en el ejemplo.
  • Seguridad: nonce, capability check, validación de tipos y sanitización (sanitize_text_field, sanitize_key, wp_kses_post, etc.).
  • Rendimiento: desactiva contadores temporales durante el batch: wp_defer_term_counting(true), wp_defer_comment_counting(true) y restaurar al final.

Ejemplo alternativo: WP-CLI para importaciones grandes

Si prefieres WP-CLI para evitar límites del servidor web, puedes crear un comando que lea el JSON y ejecute la misma lógica. Ejemplo de registro de comando (archivo CLI dentro del plugin):

// Ejemplo simplificado de comando WP-CLI
if ( defined( WP_CLI )  WP_CLI ) {
    WP_CLI::add_command( json-importer run, function( args, assoc_args ) {
        file = assoc_args[file] ?? null
        if ( ! file  ! file_exists( file ) ) {
            WP_CLI::error( Archivo no encontrado. )
        }
        content = file_get_contents( file )
        data = json_decode( content, true )
        // Reutiliza la lógica de validación e inserción mostrada arriba
        WP_CLI::success( Importación finalizada. )
    } )
}

Casos especiales y solución de problemas

  • Imágenes que no se descargan: comprueba headers remotos, CORS, bloqueos por hotlink o certificados SSL. Para SSL problemático, evita desactivar verificaciones en producción mejor arreglar certificado o descargar manualmente.
  • Duplicados: usa un campo único (external_id) y guárdalo como meta para futuras comparaciones. Alternativamente, comprueba GUID o slug.
  • Errores de tiempo de ejecución: aumenta max_execution_time para procesos puntuales o segmenta la importación en batches.
  • Tipos personalizados: asegúrate de que post_type exista antes de intentar insertar.

Plugin completo (resumen)

La unión de los fragmentos mostrados produce un plugin funcional con:

  1. Registro del menú admin y plantilla de formulario.
  2. Manejo del envío con nonce y permisos.
  3. Lectura de archivo o URL, decodificación JSON.
  4. Validación por ítem.
  5. Inserción/actualización de posts, términos, metas y thumbnails.
  6. Informe de resultados.

Notas finales

Este tutorial proporciona una base sólida y segura para crear un importador desde JSON en WordPress con validación en PHP. Personaliza las validaciones, mapeos de campos y tratamiento de medios según tu esquema de datos. Ejecuta siempre pruebas en un entorno de staging y realiza copias de seguridad antes de operar en producción.

Enlaces de referencia



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 *