You are here

feeds_para_mapper.module in Feeds Paragraphs 7

Same filename and directory in other branches
  1. 8 feeds_para_mapper.module

Allows feeds to import content to Paragraphs' fields.

File

feeds_para_mapper.module
View source
<?php

/**
 * @file
 * Allows feeds to import content to Paragraphs' fields.
 */

/**
 * Implements hook_help().
 */
function feeds_para_mapper_help($path, $arg) {
  switch ($path) {
    case 'admin/help#feeds_para_mapper':
      $filepath = dirname(__FILE__) . '/README.md';
      if (file_exists($filepath)) {
        $readme = file_get_contents($filepath);
      }
      else {
        $filepath = dirname(__FILE__) . '/README.txt';
        if (file_exists($filepath)) {
          $readme = file_get_contents($filepath);
        }
      }
      if (!isset($readme)) {
        return NULL;
      }
      if (module_exists('markdown')) {
        $filters = module_invoke('markdown', 'filter_info');
        $info = $filters['filter_markdown'];
        if (function_exists($info['process callback'])) {
          $output = $info['process callback']($readme, NULL);
        }
        else {
          $output = '<pre>' . $readme . '</pre>';
        }
      }
      else {
        $output = '<pre>' . $readme . '</pre>';
      }
      return $output;
  }
}

/**
 * Implements hook_feeds_processor_targets().
 */
function feeds_para_mapper_feeds_processor_targets($entity_type, $bundle, $with_path = FALSE) {
  $targets = array();
  $entity_fields = field_info_instances($entity_type, $bundle);
  $fields_to_include = array();

  // Search for paragraphs fields:
  foreach ($entity_fields as $entity_field) {
    if (!isset($entity_field['bundle'])) {
      continue;
    }
    if ($entity_field['bundle'] === $bundle && $entity_field['widget']['module'] === "paragraphs") {
      array_push($fields_to_include, $entity_field);
    }
  }
  if (empty($fields_to_include)) {
    return $targets;
  }

  // Get bundles' fields:
  foreach ($fields_to_include as $para_field) {
    $host_field = field_info_field($para_field['field_name']);
    $para_field_info = array(
      'name' => $para_field['label'],
      'machine_name' => $para_field['field_name'],
      'field_id' => $para_field['field_id'],
      'bundle' => $bundle,
      'entity_type' => $entity_type,
    );

    // Call hook_feeds_processor_targets on each field that support Feeds,
    // in order to display the field mapping settings.
    $sub_fields = feeds_para_mapper_get_target_fields($para_field_info, $with_path);
    foreach ($sub_fields as $sub_field) {
      $module_targets = feeds_para_mapper_call_targets_form_hook($sub_field);
      foreach ($module_targets as $name => $target) {
        $targetF = array_filter($sub_fields, function ($item) use ($name) {
          $containsKey = strpos($name, $item['machine_name']) !== FALSE;
          $sameKey = $item['machine_name'] === $name;
          return $containsKey || $sameKey;
        });
        if (count($targetF)) {
          $targetF = reset($targetF);
          $up = $target;
          $keys = array_keys($targetF);
          foreach ($keys as $key) {
            $up[$key] = $targetF[$key];
          }
          $up['callback'] = 'feeds_para_mapper_set_target';
          $up['module_callback'] = $target['callback'];
          $up['ctype_field'] = $para_field_info['machine_name'];
          $targets[$name] = $up;

          // Check if the field is multi-value field,
          // and display settings accordingly:
          if (isset($targetF['host_field'])) {
            $host_field = field_info_field($targetF['host_field']);
          }
          $targetInfo = field_info_field($targetF['machine_name']);
          $has_settings = FALSE;
          $tAllowed = (int) $targetInfo['cardinality'];
          $hAllowed = (int) $host_field['cardinality'];
          if ($hAllowed > 1 || $hAllowed === -1) {
            if ($tAllowed > 1 || $tAllowed === -1) {
              $has_settings = TRUE;
            }
          }
          if ($has_settings) {
            $form_callback = "feeds_para_mapper_form_callback";
            $sum_callback = "feeds_para_mapper_summary_callback";

            // Add the form callback:
            if (isset($targets[$name]['form_callbacks'])) {
              $targets[$name]['form_callbacks'][] = $form_callback;
            }
            else {
              $targets[$name]['form_callbacks'] = array(
                $form_callback,
              );
            }

            // Add the summary callback:
            if (isset($targets[$name]['summary_callbacks'])) {
              $targets[$name]['summary_callbacks'][] = $sum_callback;
            }
            else {
              $targets[$name]['summary_callbacks'] = array(
                $sum_callback,
              );
            }
          }
        }
      }
    }
  }
  return $targets;
}

/**
 * Searches for any fields that a Paragraphs field has.
 *
 * @param array $target
 *   The host Paragraphs field.
 * @param bool $with_path
 *   Whether we should also build the field path (for nested paragraphs fields).
 * @param array $result
 *   The previous search result.
 * @param array $first_host
 *   The first Paragraphs host, it's the $target it's bundle.
 *
 * @return array
 *   The found fields.
 */
function feeds_para_mapper_get_target_fields(array $target, $with_path = FALSE, array $result = array(), array $first_host = array()) {
  $name = $target['machine_name'];
  $bundle = $target['bundle'];
  $target_info = field_info_instance($target['entity_type'], $name, $bundle);
  $target_bundles = array_filter($target_info['settings']['allowed_bundles'], function ($item) {
    return $item !== -1;
  });
  $target_bundles = array_values($target_bundles);
  foreach ($target_bundles as $target_bundle) {
    $sub_fields = field_info_instances('paragraphs_item', $target_bundle);
    foreach ($sub_fields as $machine_name => $sub_field) {

      // Initialize first host:
      if ($target['entity_type'] !== 'paragraphs_item') {
        $first_host = array(
          'bundle' => $target_bundle,
          'host_field' => $target['machine_name'],
          'host_entity' => $target['entity_type'],
        );
      }

      // If we found nested Paragraphs field,
      // loop through it's sub fields to include them:
      if ($sub_field['widget']['module'] === 'paragraphs') {

        // Initialize path name:
        $sub_target = array(
          'machine_name' => $machine_name,
          'bundle' => $sub_field['bundle'],
          'entity_type' => "paragraphs_item",
          'host_field' => $machine_name,
        );
        $result = feeds_para_mapper_get_target_fields($sub_target, $with_path, $result, $first_host);
      }
      else {

        // We didn't find nested Paragraphs field, include this field:
        $info = field_info_field($machine_name);
        $field = array(
          'machine_name' => $machine_name,
          'paragraph_bundle' => $sub_field['bundle'],
          'module' => $info['module'],
          'type' => $info['type'],
        );
        if (isset($target['host_field'])) {
          $field['host_field'] = $target['host_field'];
        }
        if ($with_path) {
          $field['path'] = feeds_para_mapper_build_path($sub_field, $first_host);
          feeds_para_mapper_set_fields_in_common($field, $result);
        }
        $result[] = $field;
      }
    }
  }
  return $result;
}

/**
 * Creates information array about each parent host field of the target field.
 *
 * @param array $field
 *   The target field.
 * @param array $first_host
 *   The first host field.
 *
 * @return array
 *   List of the host fields.
 */
function feeds_para_mapper_build_path(array $field, array $first_host) {
  $bundles = field_info_bundles('paragraphs_item');

  // Get bundles fields:
  foreach ($bundles as $name => $bundle) {
    $bundles[$name]['name'] = $name;
    $fields = field_info_instances('paragraphs_item', $name);
    $bundles[$name]['fields'] = $fields;
  }
  $field_bundle = NULL;
  $getFieldBundle = function ($field) use ($bundles) {
    foreach ($bundles as $bundle) {
      foreach ($bundle['fields'] as $b_field) {
        if ($b_field['field_name'] === $field['field_name']) {
          return $bundle;
        }
      }
    }
    return NULL;
  };
  $getHost = function ($field_bundle) use ($bundles) {
    foreach ($bundles as $bundle) {
      foreach ($bundle['fields'] as $b_field) {
        if (isset($b_field['settings']['allowed_bundles'])) {
          foreach ($b_field['settings']['allowed_bundles'] as $allowed_bundle) {
            if ($allowed_bundle === $field_bundle['name']) {

              /*
                            Get the allowed bundle and set it as the host bundle.
                            This grabs the first allowed bundle,
                            and might cause issues with multiple bundles field.
                            todo: Test with multiple bundles field.
              */
              $allowed = array_filter($bundles, function ($item) use ($allowed_bundle) {
                return $item['name'] === $allowed_bundle;
              });
              $allowed = array_values($allowed);
              return array(
                'bundle' => $allowed[0],
                'host_field' => $b_field,
              );
            }
          }
        }
      }
    }
    return NULL;
  };

  // Start building the path:
  $path = array();
  $field_bundle = $getFieldBundle($field);
  while (isset($field_bundle)) {
    $host = $getHost($field_bundle);
    if (isset($host)) {
      $new_path = array(
        'bundle' => $host['bundle']['name'],
        'host_field' => $host['host_field']['field_name'],
        'host_entity' => 'paragraphs_item',
      );
      array_unshift($path, $new_path);
      $field_bundle = $getFieldBundle($host['host_field']);
    }
    else {
      $field_bundle = NULL;
    }
  }

  // Add the first host to the path:
  array_unshift($path, $first_host);

  // Add order to all path items:
  for ($i = 0; $i < count($path); $i++) {
    $path[$i]['order'] = $i;
  }
  return $path;
}

/**
 * Finds fields that share the same host as the target.
 *
 * @param array $field
 *   The target fields.
 * @param array $fields
 *   The other collected fields so far.
 */
function feeds_para_mapper_set_fields_in_common(array &$field, array &$fields) {
  foreach ($fields as $key => $other_field) {
    $last_key = count($other_field['path']) - 1;
    $others_host = $other_field['path'][$last_key];
    $current_host_key = count($field['path']) - 1;
    $current_host = $field['path'][$current_host_key];
    if ($others_host['host_field'] === $current_host['host_field']) {
      if (!isset($field['in_common'])) {
        $field['in_common'] = array();
      }
      if (!isset($fields[$key]['in_common'])) {
        $fields[$key]['in_common'] = array();
      }
      $field['in_common'][] = $other_field['machine_name'];
      $fields[$key]['in_common'][] = $field['machine_name'];
    }
  }
}

/**
 * Form callback for Paragraphs field targets.
 *
 * Allows to select a text format for the text field target.
 *
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param array $target
 *   The target field information.
 * @param array $form
 *   Contains the form elements for the mapping.
 * @param array $form_state
 *   Information about the current form state.
 *
 * @return array
 *   The settings fields.
 *
 * @see feeds_para_mapper_feeds_processor_targets()
 * @see feeds_para_mapper_summary_callback()
 */
function feeds_para_mapper_form_callback(array $mapping, array $target, array $form, array $form_state) {

  // Display the default value:
  $mapping['max_values'] = feeds_para_mapper_get_max_values($target['machine_name'], $mapping);

  // The settings markup:
  $escaped = array(
    '@field' => $mapping['target'],
  );
  $des = t('When @field field exceeds this number of values,
     a new paragraph entity will be created to hold the remaining values.', $escaped);
  return array(
    'max_values' => array(
      '#type' => 'textfield',
      '#title' => t('Maximum Values'),
      '#default_value' => $mapping['max_values'],
      '#description' => $des,
      '#width' => '5%',
    ),
  );
}

/**
 * Summary callback for paragraph field targets.
 *
 * Displays the settings summary.
 *
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param array $target
 *   The target field information.
 * @param array $form
 *   Contains the form elements for the mapping.
 * @param array $form_state
 *   Information about the current form state.
 *
 * @return string
 *   The summary text.
 *
 * @see feeds_para_mapper_feeds_processor_targets()
 * @see feeds_para_mapper_form_callback()
 */
function feeds_para_mapper_summary_callback(array $mapping, array $target, array $form, array $form_state) {
  $res = feeds_para_mapper_get_max_values($target['machine_name'], $mapping);
  return t('Maximum values: @res', array(
    "@res" => $res,
  ));
}

/**
 * Gets the maximum values for a field.
 *
 * Gets the maximum values a field can hold,
 * or the user choice of the maximum values.
 *
 * @param string $target
 *   The target field.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 *
 * @return int
 *   The maximum values
 */
function feeds_para_mapper_get_max_values($target, array $mapping = array()) {
  $info = field_info_field($target);
  $crd = (int) $info['cardinality'];
  if (!isset($mapping['max_values'])) {
    return $crd;
  }
  $unlimited = $crd === -1;
  $max_values = (int) $mapping['max_values'];
  $valid = $max_values <= $crd && !$unlimited && !($max_values < 0 && $crd > 0) || $unlimited && $max_values >= -1;
  if ($valid) {
    $res = $max_values;
  }
  else {
    $res = $crd;
  }
  return $res;
}

/**
 * Calls the field module to get the settings fields for the target field.
 *
 * @param array $field
 *   The field info.
 *
 * @return array
 *   The field settings markup.
 *
 * @see feeds_para_mapper_feeds_processor_targets()
 */
function feeds_para_mapper_call_targets_form_hook(array $field) {
  $hook = "feeds_processor_targets";
  $alter_hook = "feeds_processor_targets_alter";
  $targets = array();
  $args = array(
    'paragraphs_item',
    $field['paragraph_bundle'],
  );
  $field = feeds_para_mapper_get_correct_module_name($field);
  if (module_hook($field['module'], $hook)) {
    $targets = module_invoke($field['module'], $hook, $args[0], $args[1]);
  }
  elseif (module_hook($field['module'], $alter_hook)) {
    $temp =& $targets;
    $function = $field['module'] . '_' . $alter_hook;
    $function($temp, $args[0], $args[1]);
    $targets = $temp;
  }
  return $targets;
}

/**
 * Gets the correct module name for a field.
 *
 * @param array $field
 *   The field.
 *
 * @return array
 *   The field with the module name changed if it's incorrect.
 */
function feeds_para_mapper_get_correct_module_name(array $field) {

  /*
    Sometimes a field module declares a hook different from its name,
    e.g 'image' field module, which hooks into file_feeds_processor_targets.
    We add such fields to $diff_names array to be able to call that hook,
    and later on we change the $field['module']
  */
  $diff_names = array(
    array(
      'name' => 'image',
      'correct' => 'file',
    ),
  );
  foreach ($diff_names as $diff_name) {
    if ($field['module'] === $diff_name['name']) {
      $field['module'] = $diff_name['correct'];
      return $field;
    }
  }
  return $field;
}

/**
 * Sets the values for the target field.
 *
 * @param \FeedsSource $source
 *   The source instance.
 * @param object $entity
 *   The entity that is being edited or created.
 * @param string $target
 *   The target field that the source values are being mapped to.
 * @param array $values
 *   The source field values.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 */
function feeds_para_mapper_set_target(\FeedsSource $source, $entity, $target, array $values, array $mapping) {

  // check whether there are values:
  $empty = feeds_para_mapper_is_empty($values);
  if ($empty) {
    return;
  }
  $mapping = feeds_para_mapper_init_mapping($entity, $mapping);

  // Check if the field module support Feeds.
  if (!function_exists($mapping['module_callback'])) {
    $message = t('Field @field does not support Feeds', array(
      '@field' => $mapping['field'],
    ));
    drupal_set_message($message, 'error');
    return;
  }

  // Create empty paragraphs or get the currently attached to the entity.
  $paragraphs = feeds_para_mapper_init_host_paragraphs($entity, $mapping, $values);
  foreach ($paragraphs as $item) {

    // when the target field is nested, we need to get the correct entity for this field:
    $paragraph = feeds_para_mapper_get_target_paragraph($item['paragraph'], $mapping, TRUE);
    if (!count($paragraph)) {
      continue;
    }
    $paragraph = $paragraph[0];
    feeds_para_mapper_set_value($source, $entity, $paragraph, $mapping, $item['value']);
  }
}

/**
 * Checks whether the values are empty.
 *
 * @param array $values
 *   The values
 *
 * @return bool
 *   True if the values are empty.
 */
function feeds_para_mapper_is_empty($values) {
  $emptyValues = 0;
  foreach ($values as $value) {
    if (!strlen($value)) {
      $emptyValues++;
    }
  }
  return $emptyValues === count($values);
}

/**
 * Sets the values for the target field.
 *
 * @param \FeedsSource $source
 *   The source instance.
 * @param object $entity
 *   The entity that is being edited or created.
 * @param \ParagraphsItemEntity $paragraph
 *   The loaded or created Paragraphs entity.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param array $value
 *   The source field value.
 */
function feeds_para_mapper_set_value(\FeedsSource $source, $entity, \ParagraphsItemEntity $paragraph, array $mapping, array $value) {

  /*
    Call the the field module to begin mapping.
    Sometimes the field module sets a target different than the real target,
    e.g 'target_field:sub_target',
    in this case we need to pass the $mapping['target'] value.
  */
  $correct_field = $mapping['field'];
  $is_different = FALSE;
  if ($mapping['field'] !== $mapping['target']) {
    $correct_field = $mapping['target'];
    $is_different = TRUE;
  }

  // Reset the values of the field:
  if (!$entity->feeds_item->is_new && !$is_different) {
    $field = $mapping['field'];
    $lang = $mapping['language'];
    $paragraph->{$field}[$lang] = array();
  }
  $args = array(
    $source,
    $paragraph,
    $correct_field,
    $value,
    $mapping,
  );
  $function = $mapping['module_callback'];
  call_user_func($function, $args[0], $args[1], $args[2], $args[3], $args[4]);
  if (!$entity->feeds_item->is_new) {
    feeds_para_mapper_append_to_update($entity, $mapping, $paragraph);
  }
}

/**
 * Initializes the mapping array.
 *
 * @param object $entity
 *   The entity that is being edited or created.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 *
 * @return array
 *   The final mapping array.
 */
function feeds_para_mapper_init_mapping($entity, array $mapping) {
  $type = $entity->feeds_item->entity_type;
  $bundle = $entity->type;

  // Get list of the target fields.
  $cache =& drupal_static(__FUNCTION__);
  if (!isset($cache['targets'])) {

    // If the list is not cached, add it to cache.
    $cache['targets'] = feeds_para_mapper_feeds_processor_targets($type, $bundle, TRUE);
  }

  // Search for the current target in the list.
  $targets = $cache['targets'];
  $f_target = NULL;
  foreach ($targets as $name => $item) {
    if ($name === $mapping['target']) {
      $f_target = $item;
    }
  }

  // Add some required information to $mapping.
  $mapping['ctype_field'] = $f_target['ctype_field'];
  $mapping['paragraph_bundle'] = $f_target['paragraph_bundle'];
  $mapping['field'] = $f_target['machine_name'];
  $mapping['module'] = $f_target['module'];
  $mapping['type'] = $f_target['type'];
  $mapping['module_callback'] = $f_target['module_callback'];
  if (isset($f_target['real_target'])) {
    $mapping['real_target'] = $f_target['real_target'];
  }
  if (isset($f_target['path'])) {
    $mapping['path'] = $f_target['path'];
  }
  if (isset($f_target['in_common'])) {
    $mapping['in_common'] = $f_target['in_common'];
  }
  $mapping['max_values'] = feeds_para_mapper_get_max_values($mapping['field'], $mapping);
  if (isset($f_target['host_field'])) {
    $mapping['host_field'] = $f_target['host_field'];
  }
  return $mapping;
}

/**
 * Creates empty host Paragraphs entities or gets the existing ones.
 *
 * @param object $entity
 *   The entity that is being edited or created.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param array $values
 *   The values being mapped to the field.
 *
 * @return array
 *   The newly created paragraphs items.
 */
function feeds_para_mapper_init_host_paragraphs($entity, array $mapping, array $values) {
  $attached = NULL;
  $should_create = FALSE;
  $max = feeds_para_mapper_get_max_values($mapping['field'], $mapping);
  if ($max > -1) {
    $slices = array_chunk($values, $max);
  }
  else {
    $slices = array(
      $values,
    );
  }

  // Get the attached paragraphs entities:
  $attached = feeds_para_mapper_get_attached($entity, $mapping);
  if (count($attached)) {

    // Check if we should create new Paragraphs entities:
    $should_create = feeds_para_mapper_should_create_new($entity, $mapping, $slices);
  }
  if (count($attached) && !$should_create) {

    // If we loaded or found attached Paragraphs entities,
    // and don't need to create new entities:
    $items = feeds_para_mapper_update_paragraphs($mapping, $attached, $slices);
  }
  elseif (count($attached) && $should_create) {

    // If we loaded or found attached Paragraphs entities,
    // and we DO NEED to create new entities:
    $items = feeds_para_mapper_append_paragraphs($entity, $mapping, $attached, $slices);
  }
  else {
    $items = feeds_para_mapper_create_paragraphs($entity, $mapping, $slices);
  }
  return $items;
}

/**
 * Gets the parent entity for the host entity.
 *
 * @param object $entity
 *   The entity that is being edited or created.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 *
 * @return ParagraphsItemEntity|NULL
 */
function feeds_para_mapper_get_host_parent($entity, array $mapping) {

  // For first bundle fields, determine the real host:
  $parents = feeds_para_mapper_remove_existing_parents($entity, $mapping, $mapping['path'], TRUE);
  if (count($parents['removed'])) {
    $paragraph = end($parents['removed']);
    $parent = $paragraph
      ->hostEntity();
    $stop = null;
  }
  return NULL;
}

/**
 * Finds the attached paragraphs whether they are in db or just temporary attached.
 *
 * @param object $entity
 *   The entity that is being edited or created.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param bool $emptyOnly
 *   If true, we return entities where the target field is empty, otherwise we return all.
 * @param bool $firstResult
 *   If true, we return the first attached paragraph entity (first parent paragraph).
 *
 * @return array
 */
function feeds_para_mapper_get_attached($entity, $mapping, $emptyOnly = FALSE, $firstResult = FALSE) {

  // If the node entity is new, find the attached (non-saved) Paragraphs:
  if ($entity->feeds_item->is_new) {

    // Get the existing Paragraphs entity:
    $attached = feeds_para_mapper_get_target_paragraph($entity, $mapping, $emptyOnly, $firstResult);
  }
  else {

    // Load existing paragraph:
    // @todo: pass and handle the rest of params
    $attached = feeds_para_mapper_load_target_paragraph($entity, $mapping);
  }
  return $attached;
}

/**
 * Creates new Paragraphs entities, and marks others for values changes.
 *
 * @param object $entity
 *   The entity that is being edited or created.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param array $slices
 *   The sliced values based on user choice & the field cardinality.
 *
 * @return array
 *   The created Paragraphs entities based on the $slices
 */
function feeds_para_mapper_create_paragraphs($entity, array $mapping, array $slices) {
  $items = array();
  for ($i = 0; $i < count($slices); $i++) {
    $should_create = feeds_para_mapper_should_create_new($entity, $mapping, $slices, $slices[$i]);
    if (!$should_create) {
      return $items;
    }
    if ($i === 0) {

      // Create the first host Paragraphs entity/entities.
      $par = feeds_para_mapper_create_parents($mapping, $entity);
    }
    else {

      // Instead of creating another series of host entity/entities,
      // duplicate the last created host entity.
      $attached_targets = feeds_para_mapper_get_target_paragraph($entity, $mapping);
      $last = $attached_targets[count($attached_targets) - 1];
      $par = feeds_para_mapper_duplicate_existing($mapping, $entity, $last);
    }
    if ($par) {
      $items[] = array(
        'paragraph' => $par,
        'value' => $slices[$i],
      );
    }
  }
  return $items;
}

/**
 * Removes unwanted Paragraphs entities, and marks others for values changes.
 *
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param array $entities
 *   The existing Paragraphs entities.
 * @param array $slices
 *   The sliced values based on user choice & the field cardinality.
 *
 * @return array
 *   The updated entities.
 */
function feeds_para_mapper_update_paragraphs(array $mapping, array $entities, array $slices) {
  $items = array();
  $slices = feeds_para_mapper_check_values_changes($mapping, $slices, $entities);
  for ($i = 0; $i < count($slices); $i++) {
    if ($slices[$i]['state'] === "remove") {
      $entities[$i]
        ->delete();
    }
    else {
      unset($slices[$i]['state']);
      $items[] = array(
        'paragraph' => $entities[$i],
        'value' => $slices[$i],
      );
    }
  }
  return $items;
}

/**
 * Creates and updates new paragraphs entities when needed.
 *
 * Creates and marks other paragraphs entities for values changes.
 *
 * @param object $entity
 *   The entity that is being edited or created.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param array $entities
 *   The existing Paragraphs entities that are attached to the $entity.
 * @param array $slices
 *   The sliced values based on user choice & the field cardinality.
 *
 * @return array
 *   The newly created and updated entities.
 */
function feeds_para_mapper_append_paragraphs($entity, array $mapping, array $entities, array $slices) {
  $items = array();
  $slices = feeds_para_mapper_check_values_changes($mapping, $slices, $entities);
  for ($i = 0; $i < count($slices); $i++) {
    $state = $slices[$i]['state'];
    unset($slices[$i]['state']);
    $last_item = $entities[count($entities) - 1];
    if ($state === 'new') {

      // Instead of creating another series of host entity/entities,
      // duplicate the last created host entity:
      $par = feeds_para_mapper_duplicate_existing($mapping, $entity, $last_item);
      $items[] = array(
        'paragraph' => $par,
        'value' => $slices[$i],
      );
    }
    else {
      $items[] = array(
        'paragraph' => $entities[$i],
        'value' => $slices[$i],
      );
    }
  }
  return $items;
}

/**
 * Mark updated Paragraphs entity for creating new revision.
 *
 * @param object $entity
 *   The entity that is being edited or created.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param \ParagraphsItemEntity $paragraph
 *   The Paragraphs entity to mark for revisioning.
 *
 * @see feeds_para_mapper_feeds_presave()
 */
function feeds_para_mapper_append_to_update($entity, array $mapping, \ParagraphsItemEntity $paragraph) {
  if (!isset($entity->updates)) {
    $entity->updates = array();
  }
  if (isset($mapping['host_field'])) {
    $parent = $paragraph
      ->hostEntity();
  }
  else {
    $parent = $entity;
  }
  $update = array(
    'paragraph' => $paragraph,
    'parent' => $parent,
    'path' => $mapping['path'],
  );
  $entity->updates[] = $update;
}

/**
 * Checks whether we should create new Paragraphs.
 *
 * When we find existing attached paragraphs entities while updating,
 * we use this to determine if we can create new paragraph entities.
 *
 * @param object $entity
 *   The host entity.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param array $slices
 *   The sliced values based on user choice & the field cardinality.
 * @param array $futureValue
 *   The future value for the target field.
 *
 * @return bool
 *   TRUE if we should create new Paragraphs entity.
 */
function feeds_para_mapper_should_create_new($entity, array $mapping, array $slices, array $futureValue = array()) {
  if (isset($mapping['host_field'])) {
    $host_field = $mapping['host_field'];
    $host = $entity
      ->hostEntity();
  }
  else {
    $host_field = $mapping['ctype_field'];
    $host = $entity;
  }
  $current_values = array();
  if (isset($host->{$host_field})) {
    $current_values = $host->{$host_field}[$mapping['language']];
  }
  $host_field_info = field_info_field($host_field);
  $allowed = (int) $host_field_info['cardinality'];
  $skip_check = $allowed === -1;

  // If the parent cannot hold more than 1 value, we should not:
  if ($allowed === count($current_values) && !$skip_check) {
    return FALSE;
  }
  $exceeded = TRUE;
  if ($skip_check) {
    $max = $mapping['max_values'];
    $allowed = $max;

    // Compare the child entity values with max values allowed:
    if (count($futureValue)) {
      if (count($futureValue) < $max) {
        $exceeded = FALSE;
      }
    }
    else {
      $exceeded = FALSE;
    }
  }

  // If the parent or the child entity can hold more values (children),
  // and the child cannot hold values, we should:
  if (count($current_values) < count($slices) && $allowed > count($current_values) && $exceeded) {
    return TRUE;
  }

  // Now all validation passes, if the host field is unlimited, then it can hold more values:
  if ($skip_check) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Determines whether the values are new, updated, or should be removed.
 *
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param array $slices
 *   The sliced values based on user choice & the field cardinality.
 * @param array $entities
 *   The existing Paragraphs entities.
 *
 * @return array
 *   Information about each value state.
 */
function feeds_para_mapper_check_values_changes(array $mapping, array $slices, array $entities) {
  $target = $mapping['target'];
  $lang = $mapping['language'];
  $getParagraph = function ($index) use ($entities) {
    if (isset($entities[$index])) {
      return $entities[$index];
    }
    return NULL;
  };
  $getValuesState = function ($chunk, $paragraph) use ($target, $lang) {
    $state = "new";
    if (!isset($paragraph)) {
      return $state;
    }
    if (!isset($paragraph->{$target})) {
      return "shareable";
    }
    $foundValues = array();
    foreach ($chunk as $chunkVal) {
      foreach ($paragraph->{$target}[$lang] as $value) {
        $found = FALSE;
        foreach ($value as $prop) {
          if ($prop === $chunkVal && !$found) {
            $found = TRUE;
            $foundValues[] = $prop;
          }
        }
      }
    }
    if (count($foundValues) != count($chunk)) {
      $state = "changed";
    }
    else {
      $state = "unchanged";
    }
    return $state;
  };
  for ($i = 0; $i < count($slices); $i++) {
    $par = $getParagraph($i);
    $state = $getValuesState($slices[$i], $par);
    $slices[$i]['state'] = $state;
  }

  // Search for empty paragraphs:
  for ($i = 0; $i < count($entities); $i++) {
    $has_common = FALSE;
    if (isset($mapping['in_common'])) {
      $has_common = TRUE;
      $empty_commons = array();
      foreach ($mapping['in_common'] as $field) {
        if (!isset($entities[$i]->{$field})) {
          $empty_commons[] = $field;
        }
      }

      // If all other fields are empty, we should delete this entity:
      if (count($empty_commons) === count($mapping['in_common'])) {
        $has_common = FALSE;
      }
    }
    if (!isset($slices[$i]) && !$has_common) {
      $slices[$i] = array(
        'state' => 'remove',
      );
    }
  }
  return $slices;
}

/**
 * Creates a paragraph entity and its parents.
 *
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param object $entity
 *   The entity that is being edited or created.
 *
 * @return \ParagraphsItemEntity
 *   The created paragraph entity.
 */
function feeds_para_mapper_create_parents(array $mapping, $entity) {
  if (isset($GLOBALS['call_count'])) {
    $GLOBALS['call_count']++;
  }
  else {
    $GLOBALS['call_count'] = 1;
  }
  if ($GLOBALS['call_count'] === 2) {
    $stop = null;
  }
  $parents = $mapping['path'];
  $or_count = count($parents);
  $p = feeds_para_mapper_remove_existing_parents($entity, $mapping, $parents);
  $parents = $p['parents'];
  $first = NULL;
  $last = $entity;
  if (count($parents) < $or_count) {
    $last = end($p['removed']);
  }

  // For first bundle fields, determine the real host:
  if (!isset($mapping['host_field'])) {
    $filtered = array_filter($parents, function ($item) use ($mapping) {
      return $item['host_field'] === $mapping['ctype_field'];
    });
    if (count($filtered)) {
      $parents = array();
      $parents[] = $filtered[0];
    }
  }
  if (empty($parents)) {
    $parents[] = end($mapping['path']);
  }
  $parents = array_filter($parents, function ($item) {
    return isset($item['host_entity']);
  });
  foreach ($parents as $parent) {
    $last = feeds_para_mapper_create_paragraph($parent['bundle'], $parent['host_field'], $last, $parent['host_entity']);
    if (!isset($first)) {
      $first = $last;
    }
  }
  return $first;
}

/**
 * Creates and attaches a Paragraphs entity to another entity.
 *
 * @param string $bundle
 *   The target bundle.
 * @param string $field
 *   The host field.
 * @param object $host_entity
 *   The host entity.
 * @param string $host_type
 *   The host type.
 *
 * @return \ParagraphsItemEntity
 *   The created Paragraphs entity
 */
function feeds_para_mapper_create_paragraph($bundle, $field, $host_entity, $host_type = "paragraphs_item") {

  // todo: Test appending to db data:
  $created = entity_create('paragraphs_item', array(
    'bundle' => $bundle,
    'field_name' => $field,
  ));
  if ($created instanceof ParagraphsItemEntity) {
    try {
      $created
        ->setHostEntity($host_type, $host_entity);
    } catch (Exception $exception) {
      $m = t("Could't set host entity");
      drupal_set_message($m, 'error');
      drupal_set_message(t('%err', array(
        '%err' => $exception,
      )), 'error');
    }
  }
  return $created;
}

/**
 * Duplicates an existing paragraph entity.
 *
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param object $entity
 *   The entity that is being edited or created.
 * @param \ParagraphsItemEntity $existing
 *   The Paragraphs entity to duplicate.
 *
 * @return null|\ParagraphsItemEntity
 *   The duplicated entity, or null on failure.
 */
function feeds_para_mapper_duplicate_existing(array $mapping, $entity, \ParagraphsItemEntity $existing) {
  $parents = $mapping['path'];
  $p = feeds_para_mapper_remove_existing_parents($entity, $mapping, $parents);
  $parents = $p['parents'];
  $parents = array_filter($parents, function ($item) {
    return isset($item['host_entity']);
  });
  $lastKey = count($parents) - 1;
  $lastP = isset($parents[$lastKey]) ? $parents[$lastKey] : NULL;
  $parent_field = isset($mapping['host_field']) ? $mapping['host_field'] : $mapping['ctype_field'];
  $host = NULL;
  if (isset($lastP) && $existing->field_name === $lastP['host_field']) {
    $host = $existing
      ->hostEntity();
  }
  elseif ($existing->field_name === $parent_field) {
    if ($parent_field === $mapping['ctype_field']) {
      $host = $entity;
    }
    else {
      $host = $existing
        ->hostEntity();
    }
  }
  if ($host) {

    /*
        When we are inside a create loop,
        the first found entity must have a value
        if not, return it to be filled.
    */
    $target = $mapping['target'];
    $new = $entity->feeds_item->is_new;
    if (!$new && !isset($existing->{$target}) && !isset($existing->to_fill)) {
      $existing->to_fill = TRUE;
      return $existing;
    }

    // Duplicate entity:
    $h_type = $existing
      ->hostEntityType();
    return feeds_para_mapper_create_paragraph($existing
      ->bundle(), $existing->field_name, $host, $h_type);
  }
  else {
    return NULL;
  }
}

/**
 * Remove the created parents of the target field from the parents array.
 *
 * @param object $entity
 *   The entity that we are updating or creating.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param array $parents
 *   The parents of the field @see \feeds_para_mapper_build_path().
 *
 * @return array
 *   The non-existing parents array.
 */
function feeds_para_mapper_remove_existing_parents($entity, array $mapping, array $parents, $ignore_bundle = FALSE) {
  $lang = $mapping['language'];
  $findByField = function ($entity, $field, $bundle, $path = array()) use ($lang, &$findByField, $ignore_bundle) {
    $p_c = new ParagraphsItemEntity();
    $p_c = get_class($p_c);
    $found = NULL;
    if (get_class($entity) === $p_c) {
      if ($entity->field_name === $field) {
        if ($ignore_bundle) {
          return $entity;
        }
        else {
          if ($entity->bundle === $bundle) {
            return $entity;
          }
        }
      }
    }
    $props = get_object_vars($entity);
    foreach ($props as $field_name => $prop) {
      if (is_array($entity->{$field_name})) {
        $arr = $entity->{$field_name};
        if (isset($arr[$lang]) && is_array($arr[$lang])) {
          foreach ($arr[$lang] as $item) {
            $en = isset($item['entity']);
            if ($en && is_object($item['entity']) && get_class($item['entity']) === $p_c) {
              $path[] = $item['entity'];
              $found = $findByField($item['entity'], $field, $bundle, $path);
              if ($found) {
                return $found;
              }
            }
          }
        }
      }
    }
    return $found;
  };
  $removed = array();
  $to_remove = array();
  for ($i = 0; $i < count($parents); $i++) {
    $par = $findByField($entity, $parents[$i]['host_field'], $parents[$i]['bundle']);
    if ($par) {
      $removed[] = $par;
      $to_remove[] = $parents[$i]['host_field'];
    }
  }
  $parents = array_filter($parents, function ($item) use ($to_remove) {
    return !in_array($item['host_field'], $to_remove);
  });
  usort($parents, function ($a, $b) {
    return $a['order'] < $b['order'] ? -1 : 1;
  });
  return array(
    'parents' => $parents,
    'removed' => $removed,
  );
}

/**
 * Searches through nested Paragraphs entities for the target entities.
 *
 * @param object|\ParagraphsItemEntity $entity
 *   A node or paragraph object.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 * @param bool $emptyOnly
 *   If true, we return entities where the target field is empty, otherwise we return all.
 * @param bool $firstResult
 *   If true, we return the first attached paragraph entity (first parent paragraph).
 * @param array $result
 *   The previous result.
 *
 * @return array
 *   The found paragraphs.
 */
function feeds_para_mapper_get_target_paragraph($entity, array $mapping, $emptyOnly = FALSE, $firstResult = FALSE, array $result = array()) {
  $host_field = $mapping['ctype_field'];
  $bundle = $mapping['paragraph_bundle'];
  $lang = $mapping['language'];

  //  if(!isset($mapping['host_field'])){
  //    $lastPath = end($mapping['path']);
  //    $host_field = $lastPath['host_field'];
  //  }
  $pi = new ParagraphsItemEntity();
  $pi = get_class($pi);
  $isParagraph = get_class($entity) === $pi;

  // Check if nested:
  if (isset($mapping['host_field'])) {

    // If the bundle is the same as the target paragraph bundle:
    if (isset($entity->bundle) && $entity->bundle === $bundle) {

      // Check that the host field is the same:
      if ($isParagraph && $entity->field_name === $mapping['host_field']) {
        if ($emptyOnly) {
          $field = $mapping['field'];
          $values = $entity->{$field};
          if (!isset($values)) {
            $result[] = $entity;
          }
        }
        else {
          $result[] = $entity;
        }
      }
    }
    else {
      $props = get_object_vars($entity);
      foreach ($props as $prop) {
        if (is_array($prop) && isset($prop[$lang][0]['entity'])) {
          foreach ($prop[$lang] as $propVal) {
            $en = $propVal['entity'];
            $result = feeds_para_mapper_get_target_paragraph($en, $mapping, $emptyOnly, $result);
          }
        }
      }
    }
  }
  elseif (isset($entity->{$host_field})) {
    foreach ($entity->{$host_field}[$lang] as $paragraph) {
      if ($paragraph['entity']->bundle === $bundle) {
        $result[] = $paragraph['entity'];
      }
    }
  }
  elseif ($isParagraph) {
    $result[] = $entity;
  }
  return $result;
}

/**
 * Loads the existing paragraphs from db.
 *
 * @param object $entity
 *   The host entity.
 * @param array $mapping
 *   Information about the target field and the target paragraph.
 *
 * @return array
 *   The loaded Paragraphs entities.
 */
function feeds_para_mapper_load_target_paragraph($entity, array $mapping) {
  $parents = $mapping['path'];

  // Include only paragraph bundles parents:
  $parents = array_filter($parents, function ($item) {
    return isset($item['host_entity']) && $item['host_entity'] === "paragraphs_item";
  });
  $parents = array_values($parents);
  $host_field = $mapping['ctype_field'];
  $lang = $mapping['language'];
  $items = array();
  $loadParagraph = function ($id, $result = array()) use ($mapping, $parents, $lang, &$loadParagraph) {
    $paragraph = entity_load('paragraphs_item', array(
      $id,
    ));
    $paragraph = reset($paragraph);
    $target = $mapping['field'];
    $bundle = $mapping['paragraph_bundle'];
    if (isset($paragraph)) {
      if (isset($paragraph->{$target}) && $paragraph
        ->bundle() === $bundle) {
        $result[] = $paragraph;
      }
      else {
        foreach ($parents as $parent) {
          if (isset($paragraph->{$parent['host_field']})) {
            foreach ($paragraph->{$parent['host_field']}[$lang] as $item_id) {
              if (isset($item_id['value'])) {
                $id = $item_id['value'];
                $result = $loadParagraph($id, $result);
              }
            }
          }
        }
      }
    }
    return $result;
  };
  foreach ($entity->{$host_field}[$lang] as $item) {
    $res = $loadParagraph($item['value']);
    if (count($res)) {
      $items = array_merge($items, $res);
    }
  }
  return $items;
}

/**
 * Creates a revision of the Paragraphs entity when its values are changed.
 *
 * Implements hook_feeds_presave().
 */
function feeds_para_mapper_feeds_presave(FeedsSource $source, $entity, $item, $entity_id) {

  // Throw new Exception("We should not save");
  // If the entity is old, look for the paragraph item and save changes.
  if ($entity->feeds_item->is_new || !isset($entity->updates)) {
    return FALSE;
  }
  $doUpdate = function (\ParagraphsItemEntity $item) {
    $item->revision = TRUE;
    $item->default_revision = TRUE;
    try {
      $parent = $item
        ->hostEntity();
      $pi = new ParagraphsItemEntity();
      $pi = get_class($pi);
      if (isset($parent) && get_class($parent) === $pi) {
        $item
          ->save();
      }
      else {
        $item
          ->save(TRUE);
      }
      $id = array(
        $item->item_id,
      );
      $paragraph = entity_load('paragraphs_item', $id, array(), TRUE);
      $paragraph = reset($paragraph);
      $rev_id = $paragraph->revision_id;
      return $rev_id;
    } catch (Exception $exception) {
      $message = t('Failed to update the paragraph.');
      drupal_set_message($message, 'error');
      drupal_set_message($exception, 'error');
    }
    return NULL;
  };
  $toUpdate = array();
  foreach ($entity->updates as $update) {
    if (!isset($update['paragraph']->is_new)) {
      $toUpdate[] = $update;
    }
  }
  foreach ($toUpdate as $update) {
    $paragraph = $update['paragraph'];
    $doUpdate($paragraph);
  }
  return TRUE;
}

Functions

Namesort descending Description
feeds_para_mapper_append_paragraphs Creates and updates new paragraphs entities when needed.
feeds_para_mapper_append_to_update Mark updated Paragraphs entity for creating new revision.
feeds_para_mapper_build_path Creates information array about each parent host field of the target field.
feeds_para_mapper_call_targets_form_hook Calls the field module to get the settings fields for the target field.
feeds_para_mapper_check_values_changes Determines whether the values are new, updated, or should be removed.
feeds_para_mapper_create_paragraph Creates and attaches a Paragraphs entity to another entity.
feeds_para_mapper_create_paragraphs Creates new Paragraphs entities, and marks others for values changes.
feeds_para_mapper_create_parents Creates a paragraph entity and its parents.
feeds_para_mapper_duplicate_existing Duplicates an existing paragraph entity.
feeds_para_mapper_feeds_presave Creates a revision of the Paragraphs entity when its values are changed.
feeds_para_mapper_feeds_processor_targets Implements hook_feeds_processor_targets().
feeds_para_mapper_form_callback Form callback for Paragraphs field targets.
feeds_para_mapper_get_attached Finds the attached paragraphs whether they are in db or just temporary attached.
feeds_para_mapper_get_correct_module_name Gets the correct module name for a field.
feeds_para_mapper_get_host_parent Gets the parent entity for the host entity.
feeds_para_mapper_get_max_values Gets the maximum values for a field.
feeds_para_mapper_get_target_fields Searches for any fields that a Paragraphs field has.
feeds_para_mapper_get_target_paragraph Searches through nested Paragraphs entities for the target entities.
feeds_para_mapper_help Implements hook_help().
feeds_para_mapper_init_host_paragraphs Creates empty host Paragraphs entities or gets the existing ones.
feeds_para_mapper_init_mapping Initializes the mapping array.
feeds_para_mapper_is_empty Checks whether the values are empty.
feeds_para_mapper_load_target_paragraph Loads the existing paragraphs from db.
feeds_para_mapper_remove_existing_parents Remove the created parents of the target field from the parents array.
feeds_para_mapper_set_fields_in_common Finds fields that share the same host as the target.
feeds_para_mapper_set_target Sets the values for the target field.
feeds_para_mapper_set_value Sets the values for the target field.
feeds_para_mapper_should_create_new Checks whether we should create new Paragraphs.
feeds_para_mapper_summary_callback Summary callback for paragraph field targets.
feeds_para_mapper_update_paragraphs Removes unwanted Paragraphs entities, and marks others for values changes.