Migrar un campo body con imágenes y filtros a Paragraphs de distintos tipos

Introducción

Con Drupal es muy simple crear un tipo de contenido a la medida de nuestras necesidades. Con el tiempo, las exigencias pueden cambiar y con el cambio llegan más campos que enriquecen el contenido del sitio. En este artículo vamos a tocar la implementación de un tipo de campo en particular: Paragraphs. Paragraphs es seguramente uno de los campos más versátiles de Drupal. Es como tener un tipo de contenido dentro de otro, muy parecido al concepto de Field collection. En este caso, la implementación de paragraph se realiza con dos fines. Uno es el de separar el campo body diferenciando los distintos tipos de contenido, como el mismo texto, el código fuente y las imágenes. Y el otro fin es el de dar más libertad a los editores del sitio sobre la posición de cada campo, ya que con paragraph, el orden de los campos puede personalizarse de node a node.

Crear los Paragraphs con sus campos

Imaginemos por un momento que tenemos nuestro sitio con miles de artículos en un tipo de contenido que llamaremos book. En este tipo de contenido tenemos un campo body, donde se escribe texto, código fuente e imágenes (a través de un filtro). Para este caso, será necesario crear 3 paragraphs bundles.

  • Text
    • field_paragraph_text
  • Image
    • field_paragraph_image
    • field_paragraph_caption
  • Code
    • field_paragraph_code
    • field_paragraph_language

Crear un campo paragraph con PHP

Imaginemos ahora que tenemos un campo de tipo paragraph llamado field_book_paragraph dentro de nuestro tipo de contenido book. Este campo hace referencia a un paragraph bundles llamado text. Básicamente, lo que se hace es crear primero el campo paragraph del mismo modo que lo hacemos para un node y luego indicamos el node al que será vinculado nuestro campo praragraph.

$node = node_load($nid);
$text = "Hola mundo"

$paragraphs = new ParagraphsItemEntity(array('field_name' => 'field_book_paragraph', 'bundle' => 'text'));
$paragraphs->is_new = TRUE;
$paragraphs->field_p_text[LANGUAGE_NONE][] = array(
  'value'  => $text,
  'format' => 'full_html',
);
$paragraphs->setHostEntity('node', $node);
$paragraphs->save();

Script para migrar body a paragraph

Una vez que tenemos los paragraphs definidos con sus respectivos campos vamos a crear un script en PHP que recorrerá todos los nodes del tipo de contenido book. Capturará el contenido del campo body. Luego lo dividirá de modo tal que cada parte represente el contenido de los nuevos campos (que podrá ser texto, código fuente o una imagen). Por último, creará los campos para cada paragraph y lo relacionará al node que se está manipulando.

<?php

header("Content-Type: text/plain");
define('DRUPAL_ROOT', '.');
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';

// put yoour domain here
$_SERVER['HTTP_HOST'] = 'dominio.com';

$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['SCRIPT_NAME'] = '/' . basename(__FILE__);
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

ini_set('max_execution_time', '240');
ini_set('memory_limit', '512M');

global $user;

// load all nodes from content type book
$nodes = node_load_multiple(array(), array('type' => 'book'));

foreach ($nodes as $nid => $node_obj){

  // load node obj
  $node = node_load($nid);

  // manipulate only if the node have no paragraph fields
  if (count($node->field_book_paragraph) == 0 || empty($node->field_book_paragraph[LANGUAGE_NONE][0]['value']) ){

    print "Procesando node id: " . $nid . "\n";
    
    if (empty($node->body[LANGUAGE_NONE][0]['value'])){
      print "Body esta vacio.. \n";
      continue;
    }

    $body = $node->body[LANGUAGE_NONE][0]['value'];
    
    // agrego una referencia cuando cambia el campo para despues dividir
    $body = preg_replace('/\<p\>\[image (.*)\]\<\/p\>/', '<----dividi---->$1<----dividi---->', $body);
    $body = str_replace("<pre", "<----dividi----><pre", $body);
    $body = str_replace("</pre>", "</pre><----dividi---->", $body);

    $body_parts = preg_split('/<----dividi---->/', $body);
    
    for($n = 0;$n < count($body_parts); $n++) {
    
      $body_part = trim($body_parts[$n]);
      
      $type = "text";
      if (strpos($body_parts[$n], "<pre") === 0) {
        $type = "code";
      }
      if (strpos($body_parts[$n], "article|public") === 0 || strpos($body_parts[$n], "inline|public") === 0) {
        $type = "image";
      }

      switch($type) {
        case "text":
          
          $paragraphs = new ParagraphsItemEntity(array('field_name' => 'field_book_paragraph', 'bundle' => 'text'));
          $paragraphs->is_new = TRUE;
          $paragraphs->field_p_text[LANGUAGE_NONE][] = array(
            'value' => trim($body_part),
            'format' => 'article',
          );
          $paragraphs->setHostEntity('node', $node);
          $paragraphs->save();
          
          break;

        case "code":
          
          $regex = '#<pre(.*?)>([\s\S]*)</pre>#';
          $code = preg_match($regex, $body_parts[$n], $matches);

          if (count($matches) != 3) {
            print "Algo mal en el codigo de {$nid}\n";
          }
          
          $paragraphs = new ParagraphsItemEntity(array('field_name' => 'field_book_paragraph', 'bundle' => 'code'));
          $paragraphs->is_new = TRUE;
          $paragraphs->field_p_code[LANGUAGE_NONE][] = array(
            'value'  => trim($matches[2]),
            'format' => 'code',
          );
          $paragraphs->setHostEntity('node', $node);
          $paragraphs->save();

          // falta crear el campo field_p_code_language
          break;

        case "image":
          
          $image_parts = explode("|", $body_part);
          
          $image_uri = $image_parts[1];
          $image_caption = $image_parts[2];
          
          // determinar el fid a partir de la uri del archivo
          $result = db_query('SELECT f.fid FROM {file_managed} f WHERE f.uri = :uri', array(':uri' => $image_uri));
          $record = $result->fetchObject();  

          // cargar el obj file
          $file = file_load($record->fid);

          if (isset($file->filename)) {
            if (empty(trim($image_caption))) {
              print "Falta caption en {$nid}\n";
            }
            $paragraphs = new ParagraphsItemEntity(array('field_name' => 'field_book_paragraph', 'bundle' => 'image'));
            $paragraphs->is_new = TRUE;

            $paragraphs->field_p_image[LANGUAGE_NONE][] = array(
              'fid' => $file->fid,
              'filename' => $file->filename,
              'filemime' => $file->filemime,
              'uid' => 1,
              'uri' => $file->uri,
              'status' => 1
            );
            $paragraphs->field_p_caption[LANGUAGE_NONE][] = array(
              'value' => trim($image_caption),
              'format' => 'minimal',
            );
            $paragraphs->setHostEntity('node', $node);
            $paragraphs->save();
          }

          break;
      }
    }

  } else {
    print $nid." ya tiene el suyo\n";
  }
}

Por qué separar el texto del código fuente?

En linea de principio, colocar código fuente dentro del campo de texto como body no está mal. Pero separarlo del texto utilizando paragraphs puede darnos algunas ventajas y evitarnos futuros dolores de cabeza. Si por ejemplo utilizamos un editor WYSIWYG como CkEditor, tener el código fuente fuera de él en un textarea sin formato nos dará una interfáz mucho más amigable. Otro punto positivo es la facilidad de administrar la vista del código fuente sin tener que preocuparnos por el body. Podemos por ejemplo crear un archivo tpl solo para el paragraph del código fuente y utilizar prism como syntax highlighter. Tener separado todo lo que es texto de lo que es código fuente nos permitirá poder configurar search api incluyendo ambos campos pero filtrando el HTML solo en el paragraph de texto.

Conclusión

El plugin code snippet es muy bonito y hasta muy amigable. Más de 300.000 descargaron el módulo insert para incrustar imágenes en un campo de texto (blob!). Pero la verdad, nunca es una buena práctica mezclar peras con manzanas.