Ahora empieza la locura: Cambiar de Toolset Types a ACF (parte 3: migración)

Esta entrada es parte de una serie de entradas de mi experiencia de cómo trasladar de Toolset Types a ACF. No es 100% necesario leer las anteriores, pero si te enfrentas a este mismo traslado, es recomendable que leas las anteriores partes.

Ya tenemos todo montado y preparado. Nuestra estructura está lista y vemos que tenemos interfaces duplicadas para cada plugin. Pero nos extraña que ACF no tenga datos… Si los tipos de datos y las taxonomías ya aparece todo. Y es que estos últimos son unidades de información que solo pueden estar hechas de una forma: la forma de WordPress. Pero para los campos personalizados es otra historia porque es un lienzo blanco para los desarrolladores.

Como vimos en la anterior entrada, Toolset Types guarda sus campos personalizados en la tabla de postmeta y todos sus campos están marcados con el prefijo ‘wpcf-‘ (WordPress Custom Field). ACF también guarda los campos en la misma tabla, pero usa otras nomenclaturas sin prefijos y añade otros metas adicionales para su administración. Al no coincidir la clave donde guardan los dos plugins, ACF no encontrará nunca los datos que teníamos antes. Por lo que necesitamos trasladar/migrar toda la información a ACF.

Pero antes…

Antes de ponernos en faena, sé que hay muchas cosas por ahí listas. A lo mejor no te gusta ACF y quieres pasarte a Meta Box, que tiene un migrador automático. Te lo puedo comprar si no tienes programadores o alguien que te eche una mano con el código. Pero es ideal saber lo que haces y cambiarte a una herramienta que sepas utilizar.

Elige el plugin que te va a acompañar durantes meses o años. Ahora que tienes todas la estructura montada, es el momento de ver si te sirve o no ese plugin. Estamos a tiempo de equivocarnos y cambiar de plugin. Sé que es demasiado tarde si llevas una paliza, pero yo te digo que no. Es el momento porque ya has practicado a montar tipos de dato, taxonomías, campos personalizados y, posiblemente, otras cosas como relaciones y campos repetitivos. ¿Es el plugin que te permite todo eso? Entonces podemos continuar. ¿No te convence? ¿Por qué no haces una copia de la base de datos con esta instalación preparada (o exportas la configuración) y empiezas de nuevo con otro plugin?

Ahora sí, nos toca desempolvar PHP.

Mi idea de cómo trasladar de Toolset Types a ACF

Una pequeña advertencia de que esto no es ni la mejor, ni la peor, ni la más óptima, ni la de mejor rendimiento ni ningún calificativo más. Esta es una forma más de llevar información del punto A al punto B. Mi idea de este tipo de traslado es que no hay que hacer algo mega fancy (que esté muy bien hecho y que sea la delicia de un programador). Mi idea es que sea sencillo, con comprobaciones necesarias de que estamos haciendo las cosas bien y trasladar toda la información. Sin complejidades necesarias. Al fin y al cabo, solo trasladamos texto plano ahora mismo e IDs.

¿Cómo migrar los datos del tipo de dato?

Vamos a abordar la idea creando un script de PHP. Puedes orientarlo de dos formas: o para ver y ejecutar en un navegador o para ejecutarlo en la terminal. Si no estás acostumbrado a estas cosas, te recomendaré hacerlo con un script para navegador. Lo que podrás hacer es utilizar HTML para montarte vistas para ir debuggeando y viendo qué estás haciendo. Montar tablas o ir imprimiendo variables con var_dump o el juego de <pre>print_r</pre>. Que no te de miedo a probar antes de nada, recuerda que lo mejor es que pruebes a estar en local y hagas todas las pruebas. Nuestra misión es la integridad de datos.

¿Vamos a ponernos ya o qué…? Que llevo ya aquí una hora.

A mi me gusta crearme una carpeta ‘imports’ en la raíz del proyecto y empezar a crear ahí ficheros por cada tipo de dato, para separarlos e ir haciendo poco a poco. Cuando tengas el primer tipo de dato listo, lo demás será copiar y pegar de los otros ficheros.

Lo primero sería empezar el fichero añadiendo la instalación de WordPress, obvio. Para ello empezamos nuestro fichero con lo siguiente. En mi caso, empezaré con el tipo de dato disco. Hay otras formas, pero lo mantengo simple, no voy a publicar o lanzar este sistema a ningún lado.

<?php 

  // File: imports/disco.php

  include_once('../wp-load.php');

Esto hará que cargue todo WordPress antes de nuestro script. Así, podremos hacer las consultas con las funciones de WordPress conectado y sin tocar la base de datos directamente. (No SQL, please. Tampoco hace falta complicarse tanto, aunque sea más rápido).

El siguiente paso sería obtener todos los items del tipo de dato. Puedes utilizar The Loop personalizado o puedes utilizar get_posts, recuerda que cada uno de estos tiene sus propios argumentos y es posible que haya cosas que no sean iguales si decides usar otro que no sea el que use yo. Nada raro, pero tienes la documentación para ello.

En mi caso, utilizaré get_posts para tener un control total sobre la ID en la que estamos, así no dependeré de que esté cargada globalmente en el bucle y se pierda por otro lado por algún argumento. (Repito: no pretendo ser lo mejor de lo mejor, sino lo más cómodo).

Para ello, utilizaré los siguientes argumentos:

  • post_type: el tipo de dato que vamos a trasladar
  • posts_per_page: -1 para que nos traiga todos
  • post_status: solo los publicados, no tenemos ningun borrador. Si sabes que tienes otros estados, añádelos todos.
  • fields: solo que nos devuelva las IDs de la consulta, ya que vamos a utilizarla para recabar todos los campos personalizados.
  $discos = get_posts(
      array(
          'post_type' => 'disco',
          'posts_per_page' => -1,
          'post_status' => 'publish',
          'fields' => 'ids'
      )
  );

Como me gusta ser un poco tiquismiquis, me gusta añadir de que no continue el scripts si no hay discos. Esto te ayudará si, por algún motivo, la consulta no está bien hecha y no empiece a ejecutar el código así como así.

  if(empty($discos)) {
      echo 'No hay discos';
      die();
  }

  foreach($discos as $disco) {
      // Código para cada disco
  }

Obtener los datos para trabajar cómodamente

Antes de nada, aquí será en el que seré más extenso. Te mostraré todo lo que puedes hacer y, luego, en los otros nos pondremos más técnicos.

Vamos a obtener el dato de texto simple inicial con la función de Toolset. No vamos a atacar los postmeta directamente. Fíjate cómo, en el array, le indico el ID del disco actual. Si lo hiciéramos con WP_Query, no haría falta indicarlo. Pero así me aseguro yo visualmente que estoy indicando ese campo justo para ese item.

  // Dentro del bucle foreach

  $disc_id = types_render_field('disc-id', array('id' => $disco, 'output' => 'raw'));

Aquí podrías hacer ya la primera comprobación haciendo un var_dump para ver si te ha traído los campos. Esto es un ejemplo y podrías añadirle a la tabla tantas filas como campos tienes que trasladar.

  // Dentro del bucle foreach

  // $disc_id estará por aquí y otros campos
  ?>
      <table>
          <thead>
              <tr>
                  <th>Clave</th>
                  <th>Valor</th>
              </tr>
          </thead>
          <tbody>
              <tr>
                  <td>disc-id</td>
                  <td><?php echo $disc_id; ?></td>
              </tr>
              /* Añade más campos aquí */
          </tbody>
      </table>

  <?php
  // fin del contenido del bucle

Un ejemplo para mi traslado completo, es la siguiente tabla con todos los tipos de datos: (como podrás comprobar, tienes ya fechas, urls, imágenes).

La tabla solo sirve para ver que está todo OK. Si ya sabes que tienes los campos, solo te faltan retocar las funciones para obtener la información que iremos viendo poco a poco con cada uno de los campos.

Trasladar campo de texto simple

En este caso, el código de arriba es el que necesitas para obtener el campo. Para trasladarlo a ACF, utilizaremos la propia función de ACF update_field.

Como me gusta ser un poco formal, pregunto primero si los campos estarán vacíos para evitar hacer acciones de más. Esto acelerá el proceso. (Buena práctica)

Aquí te recomiendo encarecidamente que no copies-pegues solamente (esto no es StackOverflow). Entiende la función. Los parámetros son los siguientes:

  1. Nombre del campo personalizado en ACF. Ojito con esto.
  2. El valor del campo que tenemos en Toolset.
  3. ID de la entrada del tipo de dato (la que nos da nuestro foreach.
  // Dentro del bucle foreach

  // $disc_id estará por aquí y otros campos

  if(!empty($disc_id)){
    update_field('disc-id', $disc_id, $disco);
  }

  // fin del contenido del bucle

Trasladar campo de enlace o URL

Aquí no hay diferencias a un campo simple. Pero te lo dejo aquí porque son los campos que tenemos en este tipo de dato e iré apuntando por si hubiera algo diferente.

Trasladar campo de fecha

Aquí tienes que tener en cuenta una cosa: tienes Toolset que te va a dar una fecha según la has añadido y luego tienes ACF que tienes formato de entrada, de salida y de como lo tengas configurado.

El problema de las fechas es empezar a trasladarlo del formato de un plugin al otro formato. Todos sabemos que se lía cuando dejas que un mes y un día se ordene de una forma u otra. En este caso, yo te recomendaría trabajar con Unix (que es como los dos plugins tienen la fecha añadida) y actualizar directamente así.

<?php 
  // Dentro del bucle foreach

  // $disc_id estará por aquí y otros campos

  // Date field
  $disc_date_added = types_render_field('disc-date_added', array('id' => $disco, 'output' => 'raw', 'format' => 'U'));

  if (!empty($disc_date_added)) {
    update_field('disc-date_added', $disc_date_added, $disco);
  }

  // fin del contenido del bucle

Fíjate cómo en el array de argumentos para Toolset, le hemos añadido el formato U de Unix. En este paso, te diré que te recomiendo ir directamente al administrador y compruebes la fecha de que está todo OK para seguir continuando.

Trasladar campo de imagen

Ay… empezamos mal… Por desgracia, este campo de Toolset es horrible. Solo guarda la URL del medio, pero no el medio en sí, que sería lo más óptimo para poder trabajar al 100%. Por supuesto, con la función de Toolset puedes hacer miniaturas y todo lo que necesites, pero fuera del sistema WordPress: ni siquiera podrás regenerar miniaturas… (Como se hacía antiguamente en el editor clásico).

Con la función anterior, obtendremos la URL, pero no queremos que sea una URL en ACF, queremos que sea una Imagen que podamos manipular. Para ello, añadiremos una función antes del bucle que utilizaremos para obtener la ID del medio y poder añadírsela a ACF. Todo mejoras.

  // inicio del fichero

  /* Types: obtener ID de la imagen a través de URL obtenida */
  if (!function_exists('types_get_id_from_image')) {
    function types_get_id_from_image($image)
    {
      global $wpdb;

      $attachment_id = attachment_url_to_postid($image);

      if ($attachment_id != 0) {
        return intval($attachment_id);
      }

      // Comprobamos si es un medio normal
      $attachment_id = $wpdb->get_var($wpdb->prepare("SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND guid=%s", $image));

      // Si encontramos, devolvemos
      if (!empty($attachment_id)) {
        return intval($attachment_id);
      }

      // No hay nada, devolvemos falso
      return false;
    }
  }

  // nuestro bucle foreach

Internet está plagado de funciones como ésta (al igual que mis antiguos proyectos). Unas tienen mejores funciones, más cosas, menos cosas… Esta es la mía. La idea es que la función te devolverá el ID del medio o false si no está.

Una vez que tenemos la función, añadiremos el código de actualización de la imagen en ACF. Como curiosidad, este campo es una automatización que se descarga la imagen externa de otro servidor que tiene la URL en otro campo personalizado donde aquí sube la imagen.

  // inico de foreach

  // Image field
  $disc_cover_image = types_render_field('disc-cover_image-img', array('id' => $disco, 'output' => 'raw'));

  // Obtenemos la ID del medio
  $disc_cover_image_id = types_get_id_from_image($disc_cover_image);

  // Empty detectará false como vacío, por lo que si tenemos una ID, podremos actualizar el campo
  if (!empty($disc_cover_image_id)) {
    update_field('disc-cover_image-img', $disc_cover_image_id, $disco);
  }

  // fin de foreach

Qué alegría va a ser en la siguiente entrada cuando pueda quitarme esa función extra del principio y ya poder trabajar con medios, añadirle clases, atributos, permitir a otros plugins a manejar las imágenes. ¡Qué alegría!

Trasladar campos de galerías

Y si eso nos pasa con una imagen, imagina con toda una galería. Aquí he querido añadirte más funcionalidades. He cambio de tipo de dato por un segundo para que te enseñe cómo hacemos la galería. En este caso, estamos en el tipo de dato ‘Concierto’ y tengo una galería de imágenes hechas durante el mismo.

Te explico el código poco a poco, pero verás que es exactamente lo mismo:

  // inico de foreach

  // concert-gallery
  $concert_gallery = types_render_field('concert-gallery', array('id' => $current_concert, 'output' => 'raw'));

  // Creamos un array para guardar los IDs de las imágenes
  $gallery_to_acf = array();

  // Si hay imágenes en la galería de Toolset
  if (!empty($concert_gallery)) {
    // Convertimos el string en un array. Recuerda que Toolset guarda las URLs de las imágenes separadas por un espacio
    $gallery_array = explode(' ', $concert_gallery);

    // Recorremos el array de URLs
    foreach ($gallery_array as $gallery_url) {
      // ¿Recuerdas la función types_get_id_from_image() que creamos en el snippet anterior?
      $has_attachment = types_get_id_from_image($gallery_url);

      // Si la función nos devuelve un ID, lo guardamos en el array
      if (!empty($has_attachment)) {
        $gallery_to_acf[] = $has_attachment;
      }
    }
  }

  // Si el array no está vacío, actualizamos el campo de ACF. ACF espera un array de IDs de medios, sin claves ni nada.
  if (!empty($gallery_to_acf)) {
    update_field('concert-gallery', $gallery_to_acf, $current_concert);
  }

  // fin de foreach

Trasladar campos de grupos repetitivos

Los campos repetitivos de Toolset es un mundo interesante. La verdad es que están montados guay y tienes muchas más cosas, peeeero ACF lo tiene todo en un array. Ole. Manejar un array mola más que tener ahora que hacer una consulta de consultas y consultas. Quieras o no.

Entonces, ¿qué tenemos que hacer? Montarnos un array con toda la información que tenga Toolset para dársela a ACF.

Para empezar a traernos información de campos repetitivos, necesitamos utilizar la función toolset_get_related_post. Para ponerte en contexto, el campo repetitivo que tengo es la lista de canciones (tracklist) de un disco.

  // inicio de foreach

  // Repeatable field
  $disc_tracks_for_acf = array();

  // Función para obtener todas las filas
  $disc_tracks =
    toolset_get_related_posts(
    $disco,           // la ID del disco
    'tracklist',  // el nombre del campo de relación repetitiva
    'parent',         // Como tenemos el tipo de dato queremos obtener todos los campos que depende de él
    -1,               // el número de resultados
    0,                // si necesitamos un offset
    array(),          // argumentos adicionales
    'post_id',        // cómo nos devolverá los resultados
    'child',          // qué queremos que nos devuelva, en este caso, los hijos
    'rfg_order',      // Para mostrar el orden puesto dentro del front-end admin
    'ASC'             // Orden ascendente
  );

  // Comprobamos si hay resultados
  if (!empty($disc_tracks)) {
    // Recorremos todas las canciones
    foreach($disc_tracks as $current_track){
      // Nos traemos los campos de cada canción
      $song_position = types_render_field('song-position', array('id' => $current_track, 'output' => 'raw'));
      $song_type = types_render_field('song-type', array('id' => $current_track, 'output' => 'raw'));
      $song_title = types_render_field('song-title', array('id' => $current_track, 'output' => 'raw'));
      $song_duration = types_render_field('song-duration', array('id' => $current_track, 'output' => 'raw'));

      // Y los añadimos a un array para incluirlo en ACF
      $disc_tracks_for_acf[] = array(
        'song_position' => $song_position,
        'song_type' => $song_type,
        'song_title' => $song_title,
        'song_duration' => $song_duration
      );
    }
  }

  // Si tenemos canciones, las actualizamos
  if(!empty($disc_tracks_for_acf)){
    update_field('tracklist', $disc_tracks_for_acf, $disco);
  }

  // fin de foreach

No quiero ser muy repetitivo, pero al fin y al cabo, sigue siendo lo mismo para este tipo de datos. He ido explicando el código por línea y ya estarás listo. Si tuvieras campos repetitivos dentro de campos repetitivos, dentro del bucle foreach de $disc_tracks, tendrás que hacer la misma función dentro y recorrerlo.

Trasladar campos de selección

La idea es que los selectores no son más que campos simples. Por eso se le añade el valor a mostrar y el valor en la base de datos. No cambia mucho de un campo simple. Lo único que tendrás que comprobar es que si en ACF has puesto otra clave diferente al de Toolset, necesitarás un array de equivalencias de claves Toolset => ACF.

¿Cómo migrar los datos de las taxonomías?

Si ya usas Toolset, sabrás que tenemos otra función para traer todos los campos personalizados de las taxonomías creadas con Toolset. Esta función no cambia a la de types_render_field, por eso no tiene documentación como tal. Solo que tendrás que pasarle la ID del término y en el array de parámetro el tipo de taxonomía.

Para que nos funcione el update_field en este caso, ya no le podemos pasar la ID solo. Porque por defecto es un post. En la documentación, veremos que para las taxonomías personalizada, tenemos que hacer una combinación de ‘{slug de la taxonomía}_{id de la taxonomía a actualizar}’ (sin llaves).

Ten cuidado porque ahora, en types_render_termmeta, en los parámetros no usaremos la clave ‘id’, sino ‘term_id’. Es importante que la actualices porque si no, no funcionará. Te dejo el código de mi taxonomía personalizada para que tengas cosas en cuenta:

<?php

  // File: imports/artista.php

  // Cargamos WordPress
  include_once('../wp-load.php');

  // Obtenemos los artistas
  $artistas = get_terms(array(
      'taxonomy' => 'artista',
      'hide_empty' => false, // Atento: mostrar los artistas aunque no tengan discos, queremos todos los datos
      'fields' => 'ids'
  ));

  // Si no hay artistas, salimos
  if (empty($artistas)) {
      echo 'No hay artistas';
      die();
  }

  // Recorremos todos los artistas
  foreach ($artistas as $current_artista) {
      echo '<h2>' . get_term($current_artista, 'artista')->name . '</h2>';

      // Recordatorio del term_id
      $artist_id = types_render_termmeta('artist-id', array('term_id' => $current_artista, 'output' => 'raw'));

      if (!empty($artist_id)) {
          // Recuerda la clave de {slug de la taxonomía}_{id de la taxonomía a actualizar} en el segundo parámetro
          update_field('artist-id', $artist_id, 'artista_' . $current_artista);
      }

    /* Más datos para actualizar */

      echo '<p>----</p>';
  }

¿Cómo migrar los datos de los usuarios?

En este proyecto, no tengo campos de usuarios, pero es igual que las taxonomías en ACF. Y en Toolset tendrémos que usar la función types_render_usermeta (recuadro amarillo inicial), que no dista mucho de la función de types_render_field.

La clave para los usuarios en ACF será user_{ID del usuario.}. Como siempre, toda la información en la documentación. (¿Ves por qué utilizaba get_posts? Para tener controlada la ID y no con get_the_ID())

Y ya estaría.

Al menos para mi. He trasladado los campos personalizados de los tipos de datos y taxonomías que tenía. (No los habéis visto todos porque esto es ya eterno). Pero ya he hecho las comprobaciones.

¿Qué sería ideal que hiciérais? Volver a obtener los datos de Toolset y ACF y compararlos a ver si exactamente lo mismo, hace una batería de tests y comprobaciones. Ahora que ya confirmáis que todo está OK, tocaría cambiar vuestro tema o si utilizáis editores visuales. ¿Debería de escribir otra entrada sobre mis actualizaciones en el tema? Ya veremos, hay más en el backend que en el frontend.

¡Hasta la próxima!