feeds_para_mapper.module in Feeds Paragraphs 7
Same filename and directory in other branches
Allows feeds to import content to Paragraphs' fields.
File
feeds_para_mapper.moduleView 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
Name | 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. |