drupal/core/modules/field/field.attach.inc

369 lines
14 KiB
PHP

<?php
/**
* @file
* Field attach API, allowing entities (nodes, users, ...) to be 'fieldable'.
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\entity\Entity\EntityFormDisplay;
/**
* @defgroup field_attach Field Attach API
* @{
* Operates on Field API data attached to Drupal entities.
*
* Field Attach API functions load, store, display, generate Field API
* structures, and perform a variety of other functions for field data attached
* to individual entities.
*
* Field Attach API functions generally take $entity_type and $entity arguments
* along with additional function-specific arguments. $entity_type is the type
* of the fieldable entity, such as 'node' or 'user', and $entity is the entity
* itself.
*
* An entity plugin's annotation is how entity types define if and how
* Field API should operate on their entity objects. Notably, the 'fieldable'
* property needs to be set to TRUE.
*
* The Field Attach API uses the concept of bundles: the set of fields for a
* given entity is defined on a per-bundle basis. The collection of bundles for
* an entity type is added to the entity definition with
* hook_entity_info_alter(). For instance, node_entity_info_alter() exposes
* each node type as its own bundle. This means that the set of fields of a
* node is determined by the node type.
*
* The Field API reads the bundle name for a given entity from a particular
* property of the entity object, and hook_entity_info_alter() defines which
* property to use. For instance, node_entity_info_alter() specifies:
* @code
* $info['entity_keys']['bundle'] = 'type'
* @endcode
* This indicates that for a particular node object, the bundle name can be
* found in $node->type. This property can be omitted if the entity type only
* exposes a single bundle (all entities of this type have the same collection
* of fields). This is the case for the 'user' entity type.
*
* Most Field Attach API functions define a corresponding hook function that
* allows any module to act on Field Attach operations for any entity after the
* operation is complete, and access or modify all the field, form, or display
* data for that entity and operation. For example, field_attach_view() invokes
* hook_field_attach_view_alter().
*
* @link field_language Field language API @endlink provides information about
* the structure of field objects.
*
* See @link field Field API @endlink for information about the other parts of
* the Field API.
*/
/**
* Invokes a method on all the fields of a given entity.
*
* @param string $method
* The name of the method to invoke.
* @param callable $target_function
* A function that receives an $instance object and returns the object on
* which the method should be invoked.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The fully formed $entity_type entity.
* @param mixed $a
* (optional) A parameter for the invoked method. Defaults to NULL.
* @param mixed $b
* (optional) A parameter for the invoked method. Defaults to NULL.
* @param array $options
* (optional) An associative array of additional options, with the following
* keys:
* - field_name: The name of the field whose operation should be invoked. By
* default, the operation is invoked on all the fields in the entity's
* bundle. NOTE: This option is not compatible with the 'deleted' option;
* the 'field_id' option should be used instead.
* - field_id: The ID of the field whose operation should be invoked. By
* default, the operation is invoked on all the fields in the entity's'
* bundles.
* - deleted: If TRUE, the function will operate on deleted fields as well
* as non-deleted fields. If unset or FALSE, only non-deleted fields are
* operated on.
* - langcode: A language code or an array of language codes keyed by field
* name. It will be used to narrow down to a single value the available
* languages to act on.
*
* @return array
* An array of returned values.
*/
function field_invoke_method($method, $target_function, EntityInterface $entity, &$a = NULL, &$b = NULL, array $options = array()) {
// Merge default options.
$default_options = array(
'deleted' => FALSE,
'langcode' => NULL,
);
$options += $default_options;
$entity_type = $entity->entityType();
// Determine the list of instances to iterate on.
$instances = _field_invoke_get_instances($entity_type, $entity->bundle(), $options);
// Iterate through the instances and collect results.
$return = array();
foreach ($instances as $instance) {
// Let the function determine the target object on which the method should be
// called.
$target = call_user_func($target_function, $instance);
if (method_exists($target, $method)) {
$field = $instance->getField();
$field_name = $field->getFieldName();
// Determine the list of languages to iterate on.
$available_langcodes = field_available_languages($entity_type, $field);
$langcodes = _field_language_suggestion($available_langcodes, $options['langcode'], $field_name);
foreach ($langcodes as $langcode) {
$items = $entity->getTranslation($langcode)->get($field_name);
$items->filterEmptyValues();
$result = $target->$method($items, $a, $b);
if (isset($result)) {
// For methods with array results, we merge results together.
// For methods with scalar results, we collect results in an array.
if (is_array($result)) {
$return = array_merge($return, $result);
}
else {
$return[] = $result;
}
}
}
}
}
return $return;
}
/**
* Invokes a method across fields on multiple entities.
*
* @param string $method
* The name of the method to invoke.
* @param callable $target_function
* A function that receives an $instance object and returns the object on
* which the method should be invoked.
* @param array $entities
* An array of entities, keyed by entity ID.
* @param mixed $a
* (optional) A parameter for the invoked method. Defaults to NULL.
* @param mixed $b
* (optional) A parameter for the invoked method. Defaults to NULL.
* @param $options
* (optional) An associative array of additional options, with the following
* keys:
* - field_name: The name of the field whose operation should be invoked. By
* default, the operation is invoked on all the fields in the entity's
* bundle. NOTE: This option is not compatible with the 'deleted' option;
* the 'field_id' option should be used instead.
* - field_id: The ID of the field whose operation should be invoked. By
* default, the operation is invoked on all the fields in the entity's'
* bundles.
* - deleted: If TRUE, the function will operate on deleted fields as well
* as non-deleted fields. If unset or FALSE, only non-deleted fields are
* operated on.
* - langcode: A language code or an array of language codes keyed by field
* name. It will be used to narrow down to a single value the available
* languages to act on.
*
* @return array
* An array of returned values keyed by entity ID.
*
* @see field_invoke_method()
*/
function field_invoke_method_multiple($method, $target_function, array $entities, &$a = NULL, &$b = NULL, array $options = array()) {
// Merge default options.
$default_options = array(
'deleted' => FALSE,
'langcode' => NULL,
);
$options += $default_options;
$instances = array();
$grouped_items = array();
$grouped_targets = array();
$return = array();
// Go through the entities and collect the instances on which the method
// should be called.
foreach ($entities as $entity) {
$id = $entity->id();
$entity_type = $entity->entityType();
// Determine the list of instances to iterate on.
$entity_instances = _field_invoke_get_instances($entity_type, $entity->bundle(), $options);
foreach ($entity_instances as $instance) {
$instance_uuid = $instance->uuid();
$field_name = $instance->getFieldName();
// Let the closure determine the target object on which the method should
// be called.
if (empty($grouped_targets[$instance_uuid])) {
$grouped_targets[$instance_uuid] = call_user_func($target_function, $instance);
}
if (method_exists($grouped_targets[$instance_uuid], $method)) {
// Add the instance to the list of instances to invoke the hook on.
if (!isset($instances[$instance_uuid])) {
$instances[$instance_uuid] = $instance;
}
// Unless a language code suggestion is provided we iterate on all the
// available language codes.
$field = $instance->getField();
$available_langcodes = field_available_languages($entity_type, $field);
$langcode = !empty($options['langcode'][$id]) ? $options['langcode'][$id] : $options['langcode'];
$langcodes = _field_language_suggestion($available_langcodes, $langcode, $field_name);
foreach ($langcodes as $langcode) {
// Group the items corresponding to the current field.
$items = $entity->getTranslation($langcode)->get($field_name);
$items->filterEmptyValues();
$grouped_items[$instance_uuid][$langcode][$id] = $items;
}
}
}
// Initialize the return value for each entity.
$return[$id] = array();
}
// For each instance, invoke the method and collect results.
foreach ($instances as $instance_uuid => $instance) {
// Iterate over all the field translations.
foreach ($grouped_items[$instance_uuid] as $items) {
$results = $grouped_targets[$instance_uuid]->$method($items, $a, $b);
if (isset($results)) {
// Collect results by entity.
// For hooks with array results, we merge results together.
// For hooks with scalar results, we collect results in an array.
foreach ($results as $id => $result) {
if (is_array($result)) {
$return[$id] = array_merge($return[$id], $result);
}
else {
$return[$id][] = $result;
}
}
}
}
}
return $return;
}
/**
* Retrieves a list of instances to operate on.
*
* Helper for field_invoke_method().
*
* @param $entity_type
* The entity type.
* @param $bundle
* The bundle name.
* @param $options
* An associative array of options, as provided to field_invoke_method(). Only
* the following keys are considered:
* - deleted
* - field_name
* - field_id
* See field_invoke_method() for details.
*
* @return
* The array of selected instance definitions.
*/
function _field_invoke_get_instances($entity_type, $bundle, $options) {
if ($options['deleted']) {
// Deleted fields are not included in field_info_instances(), and need to
// be fetched from the database with field_read_instances().
$params = array('entity_type' => $entity_type, 'bundle' => $bundle);
if (isset($options['field_id'])) {
// Single-field mode by field id: field_read_instances() does the filtering.
// Single-field mode by field name is not compatible with the 'deleted'
// option.
$params['field_id'] = $options['field_id'];
}
$instances = field_read_instances($params, array('include_deleted' => TRUE));
}
elseif (isset($options['field_name'])) {
// Single-field mode by field name: field_info_instance() does the
// filtering.
$instances = array(field_info_instance($entity_type, $options['field_name'], $bundle));
}
else {
$instances = field_info_instances($entity_type, $bundle);
if (isset($options['field_id'])) {
// Single-field mode by field id: we need to loop on each instance to
// find the right one.
foreach ($instances as $instance) {
if ($instance->getField()->uuid() == $options['field_id']) {
$instances = array($instance);
break;
}
}
}
}
return $instances;
}
/**
* Defines a 'target function' for field_invoke_method().
*
* Used to invoke methods on an instance's widget.
*
* @param \Drupal\entity\Entity\EntityFormDisplay $form_display
* An EntityFormDisplay object.
*
* @return callable $target_function
* A 'target function' for field_invoke_method().
*/
function _field_invoke_widget_target($form_display) {
return function ($instance) use ($form_display) {
return $form_display->getRenderer($instance->getFieldName());
};
}
/**
* Populates the template variables with the available field values.
*
* The $variables array will be populated with all the field instance values
* associated with the given entity type, keyed by field name; in case of
* translatable fields the language currently chosen for display will be
* selected.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity with fields to render.
* @param $element
* The structured array containing the values ready for rendering.
* @param $variables
* The variables array is passed by reference and will be populated with field
* values.
*/
function field_attach_preprocess(EntityInterface $entity, $element, &$variables) {
foreach (field_info_instances($entity->entityType(), $entity->bundle()) as $field_name => $instance) {
if (isset($element[$field_name]['#language'])) {
$langcode = $element[$field_name]['#language'];
$variables[$field_name] = $entity->getTranslation($langcode)->{$field_name}->getValue();
}
}
// Let other modules make changes to the $variables array.
$context = array(
'entity' => $entity,
'element' => $element,
);
drupal_alter('field_attach_preprocess', $variables, $context);
}
/**
* @} End of "defgroup field_attach".
*/