Chris@0: $choice) { Chris@0: if (is_array($choice)) { Chris@0: $options[] = [ Chris@0: 'type' => 'optgroup', Chris@0: 'label' => $key, Chris@0: 'options' => form_select_options($element, $choice), Chris@0: ]; Chris@0: } Chris@0: elseif (is_object($choice) && isset($choice->option)) { Chris@0: $options = array_merge($options, form_select_options($element, $choice->option)); Chris@0: } Chris@0: else { Chris@0: $option = []; Chris@0: $key = (string) $key; Chris@0: $empty_choice = $empty_value && $key == '_none'; Chris@0: if ($value_valid && ((!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value']))) || $empty_choice)) { Chris@0: $option['selected'] = TRUE; Chris@0: } Chris@0: else { Chris@0: $option['selected'] = FALSE; Chris@0: } Chris@0: $option['type'] = 'option'; Chris@0: $option['value'] = $key; Chris@0: $option['label'] = $choice; Chris@0: $options[] = $option; Chris@0: } Chris@0: } Chris@0: return $options; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the indexes of a select element's options matching a given key. Chris@0: * Chris@0: * This function is useful if you need to modify the options that are Chris@0: * already in a form element; for example, to remove choices which are Chris@0: * not valid because of additional filters imposed by another module. Chris@0: * One example might be altering the choices in a taxonomy selector. Chris@0: * To correctly handle the case of a multiple hierarchy taxonomy, Chris@0: * #options arrays can now hold an array of objects, instead of a Chris@0: * direct mapping of keys to labels, so that multiple choices in the Chris@0: * selector can have the same key (and label). This makes it difficult Chris@0: * to manipulate directly, which is why this helper function exists. Chris@0: * Chris@0: * This function does not support optgroups (when the elements of the Chris@0: * #options array are themselves arrays), and will return FALSE if Chris@0: * arrays are found. The caller must either flatten/restore or Chris@0: * manually do their manipulations in this case, since returning the Chris@0: * index is not sufficient, and supporting this would make the Chris@0: * "helper" too complicated and cumbersome to be of any help. Chris@0: * Chris@0: * As usual with functions that can return array() or FALSE, do not Chris@0: * forget to use === and !== if needed. Chris@0: * Chris@0: * @param $element Chris@0: * The select element to search. Chris@0: * @param $key Chris@0: * The key to look for. Chris@0: * Chris@0: * @return Chris@0: * An array of indexes that match the given $key. Array will be Chris@0: * empty if no elements were found. FALSE if optgroups were found. Chris@0: */ Chris@0: function form_get_options($element, $key) { Chris@0: $keys = []; Chris@0: foreach ($element['#options'] as $index => $choice) { Chris@0: if (is_array($choice)) { Chris@0: return FALSE; Chris@0: } Chris@0: elseif (is_object($choice)) { Chris@0: if (isset($choice->option[$key])) { Chris@0: $keys[] = $index; Chris@0: } Chris@0: } Chris@0: elseif ($index == $key) { Chris@0: $keys[] = $index; Chris@0: } Chris@0: } Chris@0: return $keys; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for fieldset element templates. Chris@0: * Chris@0: * Default template: fieldset.html.twig. Chris@0: * Chris@0: * @param array $variables Chris@0: * An associative array containing: Chris@0: * - element: An associative array containing the properties of the element. Chris@0: * Properties used: #attributes, #children, #description, #id, #title, Chris@0: * #value. Chris@0: */ Chris@0: function template_preprocess_fieldset(&$variables) { Chris@0: $element = $variables['element']; Chris@0: Element::setAttributes($element, ['id']); Chris@0: RenderElement::setAttributes($element); Chris@0: $variables['attributes'] = isset($element['#attributes']) ? $element['#attributes'] : []; Chris@0: $variables['prefix'] = isset($element['#field_prefix']) ? $element['#field_prefix'] : NULL; Chris@0: $variables['suffix'] = isset($element['#field_suffix']) ? $element['#field_suffix'] : NULL; Chris@0: $variables['title_display'] = isset($element['#title_display']) ? $element['#title_display'] : NULL; Chris@0: $variables['children'] = $element['#children']; Chris@0: $variables['required'] = !empty($element['#required']) ? $element['#required'] : NULL; Chris@0: Chris@0: if (isset($element['#title']) && $element['#title'] !== '') { Chris@0: $variables['legend']['title'] = ['#markup' => $element['#title']]; Chris@0: } Chris@0: Chris@0: $variables['legend']['attributes'] = new Attribute(); Chris@0: // Add 'visually-hidden' class to legend span. Chris@0: if ($variables['title_display'] == 'invisible') { Chris@0: $variables['legend_span']['attributes'] = new Attribute(['class' => ['visually-hidden']]); Chris@0: } Chris@0: else { Chris@0: $variables['legend_span']['attributes'] = new Attribute(); Chris@0: } Chris@0: Chris@0: if (!empty($element['#description'])) { Chris@0: $description_id = $element['#attributes']['id'] . '--description'; Chris@0: $description_attributes['id'] = $description_id; Chris@0: $variables['description']['attributes'] = new Attribute($description_attributes); Chris@0: $variables['description']['content'] = $element['#description']; Chris@0: Chris@0: // Add the description's id to the fieldset aria attributes. Chris@0: $variables['attributes']['aria-describedby'] = $description_id; Chris@0: } Chris@0: Chris@0: // Suppress error messages. Chris@0: $variables['errors'] = NULL; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for details element templates. Chris@0: * Chris@0: * Default template: details.html.twig. Chris@0: * Chris@0: * @param array $variables Chris@0: * An associative array containing: Chris@0: * - element: An associative array containing the properties of the element. Chris@17: * Properties used: #attributes, #children, #description, #required, Chris@17: * #summary_attributes, #title, #value. Chris@0: */ Chris@0: function template_preprocess_details(&$variables) { Chris@0: $element = $variables['element']; Chris@0: $variables['attributes'] = $element['#attributes']; Chris@17: $variables['summary_attributes'] = new Attribute($element['#summary_attributes']); Chris@0: if (!empty($element['#title'])) { Chris@0: $variables['summary_attributes']['role'] = 'button'; Chris@0: if (!empty($element['#attributes']['id'])) { Chris@0: $variables['summary_attributes']['aria-controls'] = $element['#attributes']['id']; Chris@0: } Chris@0: $variables['summary_attributes']['aria-expanded'] = !empty($element['#attributes']['open']) ? 'true' : 'false'; Chris@0: $variables['summary_attributes']['aria-pressed'] = $variables['summary_attributes']['aria-expanded']; Chris@0: } Chris@0: $variables['title'] = (!empty($element['#title'])) ? $element['#title'] : ''; Chris@17: // If the element title is a string, wrap it a render array so that markup Chris@17: // will not be escaped (but XSS-filtered). Chris@17: if (is_string($variables['title']) && $variables['title'] !== '') { Chris@17: $variables['title'] = ['#markup' => $variables['title']]; Chris@17: } Chris@0: $variables['description'] = (!empty($element['#description'])) ? $element['#description'] : ''; Chris@0: $variables['children'] = (isset($element['#children'])) ? $element['#children'] : ''; Chris@0: $variables['value'] = (isset($element['#value'])) ? $element['#value'] : ''; Chris@0: $variables['required'] = !empty($element['#required']) ? $element['#required'] : NULL; Chris@0: Chris@0: // Suppress error messages. Chris@0: $variables['errors'] = NULL; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for radios templates. Chris@0: * Chris@0: * Default template: radios.html.twig. Chris@0: * Chris@0: * @param array $variables Chris@0: * An associative array containing: Chris@0: * - element: An associative array containing the properties of the element. Chris@0: * Properties used: #title, #value, #options, #description, #required, Chris@0: * #attributes, #children. Chris@0: */ Chris@0: function template_preprocess_radios(&$variables) { Chris@0: $element = $variables['element']; Chris@0: $variables['attributes'] = []; Chris@0: if (isset($element['#id'])) { Chris@0: $variables['attributes']['id'] = $element['#id']; Chris@0: } Chris@0: if (isset($element['#attributes']['title'])) { Chris@0: $variables['attributes']['title'] = $element['#attributes']['title']; Chris@0: } Chris@0: $variables['children'] = $element['#children']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for checkboxes templates. Chris@0: * Chris@0: * Default template: checkboxes.html.twig. Chris@0: * Chris@0: * @param array $variables Chris@0: * An associative array containing: Chris@0: * - element: An associative array containing the properties of the element. Chris@0: * Properties used: #children, #attributes. Chris@0: */ Chris@0: function template_preprocess_checkboxes(&$variables) { Chris@0: $element = $variables['element']; Chris@0: $variables['attributes'] = []; Chris@0: if (isset($element['#id'])) { Chris@0: $variables['attributes']['id'] = $element['#id']; Chris@0: } Chris@0: if (isset($element['#attributes']['title'])) { Chris@0: $variables['attributes']['title'] = $element['#attributes']['title']; Chris@0: } Chris@0: $variables['children'] = $element['#children']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for vertical tabs templates. Chris@0: * Chris@0: * Default template: vertical-tabs.html.twig. Chris@0: * Chris@0: * @param array $variables Chris@0: * An associative array containing: Chris@0: * - element: An associative array containing the properties and children of Chris@0: * the details element. Properties used: #children. Chris@0: */ Chris@0: function template_preprocess_vertical_tabs(&$variables) { Chris@0: $element = $variables['element']; Chris@0: $variables['children'] = (!empty($element['#children'])) ? $element['#children'] : ''; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for input templates. Chris@0: * Chris@0: * Default template: input.html.twig. Chris@0: * Chris@0: * @param array $variables Chris@0: * An associative array containing: Chris@0: * - element: An associative array containing the properties of the element. Chris@0: * Properties used: #attributes. Chris@0: */ Chris@0: function template_preprocess_input(&$variables) { Chris@0: $element = $variables['element']; Chris@0: // Remove name attribute if empty, for W3C compliance. Chris@0: if (isset($variables['attributes']['name']) && empty((string) $variables['attributes']['name'])) { Chris@0: unset($variables['attributes']['name']); Chris@0: } Chris@0: $variables['children'] = $element['#children']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for form templates. Chris@0: * Chris@0: * Default template: form.html.twig. Chris@0: * Chris@0: * @param $variables Chris@0: * An associative array containing: Chris@0: * - element: An associative array containing the properties of the element. Chris@0: * Properties used: #action, #method, #attributes, #children Chris@0: */ Chris@0: function template_preprocess_form(&$variables) { Chris@0: $element = $variables['element']; Chris@0: if (isset($element['#action'])) { Chris@0: $element['#attributes']['action'] = UrlHelper::stripDangerousProtocols($element['#action']); Chris@0: } Chris@0: Element::setAttributes($element, ['method', 'id']); Chris@0: if (empty($element['#attributes']['accept-charset'])) { Chris@0: $element['#attributes']['accept-charset'] = "UTF-8"; Chris@0: } Chris@0: $variables['attributes'] = $element['#attributes']; Chris@0: $variables['children'] = $element['#children']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for textarea templates. Chris@0: * Chris@0: * Default template: textarea.html.twig. Chris@0: * Chris@0: * @param array $variables Chris@0: * An associative array containing: Chris@0: * - element: An associative array containing the properties of the element. Chris@0: * Properties used: #title, #value, #description, #rows, #cols, #maxlength, Chris@0: * #placeholder, #required, #attributes, #resizable. Chris@0: */ Chris@0: function template_preprocess_textarea(&$variables) { Chris@0: $element = $variables['element']; Chris@0: $attributes = ['id', 'name', 'rows', 'cols', 'maxlength', 'placeholder']; Chris@0: Element::setAttributes($element, $attributes); Chris@0: RenderElement::setAttributes($element, ['form-textarea']); Chris@0: $variables['wrapper_attributes'] = new Attribute(); Chris@0: $variables['attributes'] = new Attribute($element['#attributes']); Chris@0: $variables['value'] = $element['#value']; Chris@0: $variables['resizable'] = !empty($element['#resizable']) ? $element['#resizable'] : NULL; Chris@0: $variables['required'] = !empty($element['#required']) ? $element['#required'] : NULL; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns HTML for a form element. Chris@0: * Prepares variables for form element templates. Chris@0: * Chris@0: * Default template: form-element.html.twig. Chris@0: * Chris@0: * In addition to the element itself, the DIV contains a label for the element Chris@0: * based on the optional #title_display property, and an optional #description. Chris@0: * Chris@0: * The optional #title_display property can have these values: Chris@0: * - before: The label is output before the element. This is the default. Chris@0: * The label includes the #title and the required marker, if #required. Chris@0: * - after: The label is output after the element. For example, this is used Chris@0: * for radio and checkbox #type elements. If the #title is empty but the field Chris@0: * is #required, the label will contain only the required marker. Chris@0: * - invisible: Labels are critical for screen readers to enable them to Chris@0: * properly navigate through forms but can be visually distracting. This Chris@0: * property hides the label for everyone except screen readers. Chris@0: * - attribute: Set the title attribute on the element to create a tooltip Chris@0: * but output no label element. This is supported only for checkboxes Chris@0: * and radios in Chris@0: * \Drupal\Core\Render\Element\CompositeFormElementTrait::preRenderCompositeFormElement(). Chris@0: * It is used where a visual label is not needed, such as a table of Chris@0: * checkboxes where the row and column provide the context. The tooltip will Chris@0: * include the title and required marker. Chris@0: * Chris@0: * If the #title property is not set, then the label and any required marker Chris@0: * will not be output, regardless of the #title_display or #required values. Chris@0: * This can be useful in cases such as the password_confirm element, which Chris@0: * creates children elements that have their own labels and required markers, Chris@0: * but the parent element should have neither. Use this carefully because a Chris@0: * field without an associated label can cause accessibility challenges. Chris@0: * Chris@18: * To associate the label with a different field, set the #label_for property Chris@18: * to the ID of the desired field. Chris@18: * Chris@0: * @param array $variables Chris@0: * An associative array containing: Chris@0: * - element: An associative array containing the properties of the element. Chris@0: * Properties used: #title, #title_display, #description, #id, #required, Chris@18: * #children, #type, #name, #label_for. Chris@0: */ Chris@0: function template_preprocess_form_element(&$variables) { Chris@0: $element = &$variables['element']; Chris@0: Chris@0: // This function is invoked as theme wrapper, but the rendered form element Chris@0: // may not necessarily have been processed by Chris@0: // \Drupal::formBuilder()->doBuildForm(). Chris@0: $element += [ Chris@0: '#title_display' => 'before', Chris@0: '#wrapper_attributes' => [], Chris@0: '#label_attributes' => [], Chris@18: '#label_for' => NULL, Chris@0: ]; Chris@0: $variables['attributes'] = $element['#wrapper_attributes']; Chris@0: Chris@0: // Add element #id for #type 'item'. Chris@0: if (isset($element['#markup']) && !empty($element['#id'])) { Chris@0: $variables['attributes']['id'] = $element['#id']; Chris@0: } Chris@0: Chris@0: // Pass elements #type and #name to template. Chris@0: if (!empty($element['#type'])) { Chris@0: $variables['type'] = $element['#type']; Chris@0: } Chris@0: if (!empty($element['#name'])) { Chris@0: $variables['name'] = $element['#name']; Chris@0: } Chris@0: Chris@0: // Pass elements disabled status to template. Chris@0: $variables['disabled'] = !empty($element['#attributes']['disabled']) ? $element['#attributes']['disabled'] : NULL; Chris@0: Chris@0: // Suppress error messages. Chris@0: $variables['errors'] = NULL; Chris@0: Chris@0: // If #title is not set, we don't display any label. Chris@0: if (!isset($element['#title'])) { Chris@0: $element['#title_display'] = 'none'; Chris@0: } Chris@0: Chris@0: $variables['title_display'] = $element['#title_display']; Chris@0: Chris@0: $variables['prefix'] = isset($element['#field_prefix']) ? $element['#field_prefix'] : NULL; Chris@0: $variables['suffix'] = isset($element['#field_suffix']) ? $element['#field_suffix'] : NULL; Chris@0: Chris@0: $variables['description'] = NULL; Chris@0: if (!empty($element['#description'])) { Chris@0: $variables['description_display'] = $element['#description_display']; Chris@0: $description_attributes = []; Chris@0: if (!empty($element['#id'])) { Chris@0: $description_attributes['id'] = $element['#id'] . '--description'; Chris@0: } Chris@0: $variables['description']['attributes'] = new Attribute($description_attributes); Chris@0: $variables['description']['content'] = $element['#description']; Chris@0: } Chris@0: Chris@0: // Add label_display and label variables to template. Chris@0: $variables['label_display'] = $element['#title_display']; Chris@0: $variables['label'] = ['#theme' => 'form_element_label']; Chris@0: $variables['label'] += array_intersect_key($element, array_flip(['#id', '#required', '#title', '#title_display'])); Chris@0: $variables['label']['#attributes'] = $element['#label_attributes']; Chris@18: if (!empty($element['#label_for'])) { Chris@18: $variables['label']['#for'] = $element['#label_for']; Chris@18: if (!empty($element['#id'])) { Chris@18: $variables['label']['#id'] = $element['#id'] . '--label'; Chris@18: } Chris@18: } Chris@0: Chris@0: $variables['children'] = $element['#children']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Prepares variables for form label templates. Chris@0: * Chris@0: * Form element labels include the #title and a #required marker. The label is Chris@0: * associated with the element itself by the element #id. Labels may appear Chris@0: * before or after elements, depending on form-element.html.twig and Chris@0: * #title_display. Chris@0: * Chris@0: * This function will not be called for elements with no labels, depending on Chris@0: * #title_display. For elements that have an empty #title and are not required, Chris@0: * this function will output no label (''). For required elements that have an Chris@0: * empty #title, this will output the required marker alone within the label. Chris@0: * The label will use the #id to associate the marker with the field that is Chris@0: * required. That is especially important for screenreader users to know Chris@0: * which field is required. Chris@0: * Chris@18: * To associate the label with a different field, set the #for property to the Chris@18: * ID of the desired field. Chris@18: * Chris@0: * @param array $variables Chris@0: * An associative array containing: Chris@0: * - element: An associative array containing the properties of the element. Chris@18: * Properties used: #required, #title, #id, #value, #description, #for. Chris@0: */ Chris@0: function template_preprocess_form_element_label(&$variables) { Chris@0: $element = $variables['element']; Chris@0: // If title and required marker are both empty, output no label. Chris@0: if (isset($element['#title']) && $element['#title'] !== '') { Chris@0: $variables['title'] = ['#markup' => $element['#title']]; Chris@0: } Chris@0: Chris@0: // Pass elements title_display to template. Chris@0: $variables['title_display'] = $element['#title_display']; Chris@0: Chris@0: // A #for property of a dedicated #type 'label' element as precedence. Chris@0: if (!empty($element['#for'])) { Chris@0: $variables['attributes']['for'] = $element['#for']; Chris@0: // A custom #id allows the referenced form input element to refer back to Chris@0: // the label element; e.g., in the 'aria-labelledby' attribute. Chris@0: if (!empty($element['#id'])) { Chris@0: $variables['attributes']['id'] = $element['#id']; Chris@0: } Chris@0: } Chris@0: // Otherwise, point to the #id of the form input element. Chris@0: elseif (!empty($element['#id'])) { Chris@0: $variables['attributes']['for'] = $element['#id']; Chris@0: } Chris@0: Chris@0: // Pass elements required to template. Chris@0: $variables['required'] = !empty($element['#required']) ? $element['#required'] : NULL; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @defgroup batch Batch operations Chris@0: * @{ Chris@0: * Creates and processes batch operations. Chris@0: * Chris@0: * Functions allowing forms processing to be spread out over several page Chris@0: * requests, thus ensuring that the processing does not get interrupted Chris@0: * because of a PHP timeout, while allowing the user to receive feedback Chris@0: * on the progress of the ongoing operations. Chris@0: * Chris@0: * The API is primarily designed to integrate nicely with the Form API Chris@0: * workflow, but can also be used by non-Form API scripts (like update.php) Chris@0: * or even simple page callbacks (which should probably be used sparingly). Chris@0: * Chris@0: * Example: Chris@0: * @code Chris@0: * $batch = array( Chris@0: * 'title' => t('Exporting'), Chris@0: * 'operations' => array( Chris@0: * array('my_function_1', array($account->id(), 'story')), Chris@0: * array('my_function_2', array()), Chris@0: * ), Chris@0: * 'finished' => 'my_finished_callback', Chris@0: * 'file' => 'path_to_file_containing_myfunctions', Chris@0: * ); Chris@0: * batch_set($batch); Chris@0: * // Only needed if not inside a form _submit handler. Chris@0: * // Setting redirect in batch_process. Chris@0: * batch_process('node/1'); Chris@0: * @endcode Chris@0: * Chris@0: * Note: if the batch 'title', 'init_message', 'progress_message', or Chris@0: * 'error_message' could contain any user input, it is the responsibility of Chris@0: * the code calling batch_set() to sanitize them first with a function like Chris@0: * \Drupal\Component\Utility\Html::escape() or Chris@0: * \Drupal\Component\Utility\Xss::filter(). Furthermore, if the batch operation Chris@0: * returns any user input in the 'results' or 'message' keys of $context, it Chris@0: * must also sanitize them first. Chris@0: * Chris@0: * Sample callback_batch_operation(): Chris@0: * @code Chris@0: * // Simple and artificial: load a node of a given type for a given user Chris@0: * function my_function_1($uid, $type, &$context) { Chris@0: * // The $context array gathers batch context information about the execution (read), Chris@0: * // as well as 'return values' for the current operation (write) Chris@0: * // The following keys are provided : Chris@0: * // 'results' (read / write): The array of results gathered so far by Chris@0: * // the batch processing, for the current operation to append its own. Chris@0: * // 'message' (write): A text message displayed in the progress page. Chris@0: * // The following keys allow for multi-step operations : Chris@0: * // 'sandbox' (read / write): An array that can be freely used to Chris@0: * // store persistent data between iterations. It is recommended to Chris@0: * // use this instead of $_SESSION, which is unsafe if the user Chris@0: * // continues browsing in a separate window while the batch is processing. Chris@0: * // 'finished' (write): A float number between 0 and 1 informing Chris@0: * // the processing engine of the completion level for the operation. Chris@0: * // 1 (or no value explicitly set) means the operation is finished Chris@0: * // and the batch processing can continue to the next operation. Chris@0: * Chris@0: * $nodes = \Drupal::entityTypeManager()->getStorage('node') Chris@0: * ->loadByProperties(['uid' => $uid, 'type' => $type]); Chris@0: * $node = reset($nodes); Chris@0: * $context['results'][] = $node->id() . ' : ' . Html::escape($node->label()); Chris@0: * $context['message'] = Html::escape($node->label()); Chris@0: * } Chris@0: * Chris@0: * // A more advanced example is a multi-step operation that loads all rows, Chris@0: * // five by five. Chris@0: * function my_function_2(&$context) { Chris@0: * if (empty($context['sandbox'])) { Chris@0: * $context['sandbox']['progress'] = 0; Chris@0: * $context['sandbox']['current_id'] = 0; Chris@0: * $context['sandbox']['max'] = db_query('SELECT COUNT(DISTINCT id) FROM {example}')->fetchField(); Chris@0: * } Chris@0: * $limit = 5; Chris@0: * $result = db_select('example') Chris@0: * ->fields('example', array('id')) Chris@0: * ->condition('id', $context['sandbox']['current_id'], '>') Chris@0: * ->orderBy('id') Chris@0: * ->range(0, $limit) Chris@0: * ->execute(); Chris@0: * foreach ($result as $row) { Chris@0: * $context['results'][] = $row->id . ' : ' . Html::escape($row->title); Chris@0: * $context['sandbox']['progress']++; Chris@0: * $context['sandbox']['current_id'] = $row->id; Chris@0: * $context['message'] = Html::escape($row->title); Chris@0: * } Chris@0: * if ($context['sandbox']['progress'] != $context['sandbox']['max']) { Chris@0: * $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; Chris@0: * } Chris@0: * } Chris@0: * @endcode Chris@0: * Chris@0: * Sample callback_batch_finished(): Chris@0: * @code Chris@0: * function my_finished_callback($success, $results, $operations) { Chris@0: * // The 'success' parameter means no fatal PHP errors were detected. All Chris@0: * // other error management should be handled using 'results'. Chris@0: * if ($success) { Chris@0: * $message = \Drupal::translation()->formatPlural(count($results), 'One post processed.', '@count posts processed.'); Chris@0: * } Chris@0: * else { Chris@0: * $message = t('Finished with an error.'); Chris@0: * } Chris@17: * \Drupal::messenger()->addMessage($message); Chris@0: * // Providing data for the redirected page is done through $_SESSION. Chris@0: * foreach ($results as $result) { Chris@0: * $items[] = t('Loaded node %title.', array('%title' => $result)); Chris@0: * } Chris@0: * $_SESSION['my_batch_results'] = $items; Chris@0: * } Chris@0: * @endcode Chris@0: */ Chris@0: Chris@0: /** Chris@0: * Adds a new batch. Chris@0: * Chris@0: * Batch operations are added as new batch sets. Batch sets are used to spread Chris@0: * processing (primarily, but not exclusively, forms processing) over several Chris@0: * page requests. This helps to ensure that the processing is not interrupted Chris@0: * due to PHP timeouts, while users are still able to receive feedback on the Chris@0: * progress of the ongoing operations. Combining related operations into Chris@0: * distinct batch sets provides clean code independence for each batch set, Chris@0: * ensuring that two or more batches, submitted independently, can be processed Chris@0: * without mutual interference. Each batch set may specify its own set of Chris@0: * operations and results, produce its own UI messages, and trigger its own Chris@0: * 'finished' callback. Batch sets are processed sequentially, with the progress Chris@0: * bar starting afresh for each new set. Chris@0: * Chris@0: * @param $batch_definition Chris@0: * An associative array defining the batch, with the following elements (all Chris@0: * are optional except as noted): Chris@0: * - operations: (required) Array of operations to be performed, where each Chris@0: * item is an array consisting of the name of an implementation of Chris@0: * callback_batch_operation() and an array of parameter. Chris@0: * Example: Chris@0: * @code Chris@0: * array( Chris@0: * array('callback_batch_operation_1', array($arg1)), Chris@0: * array('callback_batch_operation_2', array($arg2_1, $arg2_2)), Chris@0: * ) Chris@0: * @endcode Chris@0: * - title: A safe, translated string to use as the title for the progress Chris@0: * page. Defaults to t('Processing'). Chris@0: * - init_message: Message displayed while the processing is initialized. Chris@0: * Defaults to t('Initializing.'). Chris@0: * - progress_message: Message displayed while processing the batch. Available Chris@0: * placeholders are @current, @remaining, @total, @percentage, @estimate and Chris@0: * @elapsed. Defaults to t('Completed @current of @total.'). Chris@0: * - error_message: Message displayed if an error occurred while processing Chris@0: * the batch. Defaults to t('An error has occurred.'). Chris@0: * - finished: Name of an implementation of callback_batch_finished(). This is Chris@0: * executed after the batch has completed. This should be used to perform Chris@0: * any result massaging that may be needed, and possibly save data in Chris@0: * $_SESSION for display after final page redirection. Chris@0: * - file: Path to the file containing the definitions of the 'operations' and Chris@0: * 'finished' functions, for instance if they don't reside in the main Chris@0: * .module file. The path should be relative to base_path(), and thus should Chris@0: * be built using drupal_get_path(). Chris@0: * - library: An array of batch-specific CSS and JS libraries. Chris@0: * - url_options: options passed to the \Drupal\Core\Url object when Chris@0: * constructing redirect URLs for the batch. Chris@0: * - progressive: A Boolean that indicates whether or not the batch needs to Chris@0: * run progressively. TRUE indicates that the batch will run in more than Chris@0: * one run. FALSE (default) indicates that the batch will finish in a single Chris@0: * run. Chris@0: * - queue: An override of the default queue (with name and class fields Chris@0: * optional). An array containing two elements: Chris@0: * - name: Unique identifier for the queue. Chris@0: * - class: The name of a class that implements Chris@0: * \Drupal\Core\Queue\QueueInterface, including the full namespace but not Chris@0: * starting with a backslash. It must have a constructor with two Chris@0: * arguments: $name and a \Drupal\Core\Database\Connection object. Chris@0: * Typically, the class will either be \Drupal\Core\Queue\Batch or Chris@0: * \Drupal\Core\Queue\BatchMemory. Defaults to Batch if progressive is Chris@0: * TRUE, or to BatchMemory if progressive is FALSE. Chris@0: */ Chris@0: function batch_set($batch_definition) { Chris@0: if ($batch_definition) { Chris@0: $batch =& batch_get(); Chris@0: Chris@0: // Initialize the batch if needed. Chris@0: if (empty($batch)) { Chris@0: $batch = [ Chris@0: 'sets' => [], Chris@0: 'has_form_submits' => FALSE, Chris@0: ]; Chris@0: } Chris@0: Chris@0: // Base and default properties for the batch set. Chris@0: $init = [ Chris@0: 'sandbox' => [], Chris@0: 'results' => [], Chris@0: 'success' => FALSE, Chris@0: 'start' => 0, Chris@0: 'elapsed' => 0, Chris@0: ]; Chris@0: $defaults = [ Chris@0: 'title' => t('Processing'), Chris@0: 'init_message' => t('Initializing.'), Chris@0: 'progress_message' => t('Completed @current of @total.'), Chris@0: 'error_message' => t('An error has occurred.'), Chris@0: ]; Chris@0: $batch_set = $init + $batch_definition + $defaults; Chris@0: Chris@0: // Tweak init_message to avoid the bottom of the page flickering down after Chris@0: // init phase. Chris@0: $batch_set['init_message'] .= '
 '; Chris@0: Chris@0: // The non-concurrent workflow of batch execution allows us to save Chris@0: // numberOfItems() queries by handling our own counter. Chris@0: $batch_set['total'] = count($batch_set['operations']); Chris@0: $batch_set['count'] = $batch_set['total']; Chris@0: Chris@0: // Add the set to the batch. Chris@0: if (empty($batch['id'])) { Chris@0: // The batch is not running yet. Simply add the new set. Chris@0: $batch['sets'][] = $batch_set; Chris@0: } Chris@0: else { Chris@0: // The set is being added while the batch is running. Insert the new set Chris@0: // right after the current one to ensure execution order, and store its Chris@0: // operations in a queue. Chris@0: $index = $batch['current_set'] + 1; Chris@0: $slice1 = array_slice($batch['sets'], 0, $index); Chris@0: $slice2 = array_slice($batch['sets'], $index); Chris@0: $batch['sets'] = array_merge($slice1, [$batch_set], $slice2); Chris@0: _batch_populate_queue($batch, $index); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Processes the batch. Chris@0: * Chris@0: * This function is generally not needed in form submit handlers; Chris@0: * Form API takes care of batches that were set during form submission. Chris@0: * Chris@0: * @param \Drupal\Core\Url|string $redirect Chris@17: * (optional) Either a path or Url object to redirect to when the batch has Chris@17: * finished processing. For example, to redirect users to the home page, use Chris@17: * ''. If you wish to allow standard form API batch handling to occur Chris@17: * and force the user to be redirected to a custom location after the batch Chris@17: * has finished processing, you do not need to use batch_process() and this Chris@17: * parameter. Instead, make the batch 'finished' callback return an instance Chris@17: * of \Symfony\Component\HttpFoundation\RedirectResponse, which will be used Chris@0: * automatically by the standard batch processing pipeline (and which takes Chris@17: * precedence over this parameter). If this parameter is omitted and no Chris@17: * redirect response was returned by the 'finished' callback, the user will Chris@17: * be redirected to the page that started the batch. Any query arguments will Chris@17: * be automatically persisted. Chris@0: * @param \Drupal\Core\Url $url Chris@0: * (optional) URL of the batch processing page. Should only be used for Chris@0: * separate scripts like update.php. Chris@0: * @param $redirect_callback Chris@0: * (optional) Specify a function to be called to redirect to the progressive Chris@0: * processing page. Chris@0: * Chris@0: * @return \Symfony\Component\HttpFoundation\RedirectResponse|null Chris@0: * A redirect response if the batch is progressive. No return value otherwise. Chris@0: */ Chris@0: function batch_process($redirect = NULL, Url $url = NULL, $redirect_callback = NULL) { Chris@0: $batch =& batch_get(); Chris@0: Chris@0: if (isset($batch)) { Chris@0: // Add process information Chris@0: $process_info = [ Chris@0: 'current_set' => 0, Chris@0: 'progressive' => TRUE, Chris@0: 'url' => isset($url) ? $url : Url::fromRoute('system.batch_page.html'), Chris@0: 'source_url' => Url::fromRouteMatch(\Drupal::routeMatch())->mergeOptions(['query' => \Drupal::request()->query->all()]), Chris@0: 'batch_redirect' => $redirect, Chris@0: 'theme' => \Drupal::theme()->getActiveTheme()->getName(), Chris@0: 'redirect_callback' => $redirect_callback, Chris@0: ]; Chris@0: $batch += $process_info; Chris@0: Chris@0: // The batch is now completely built. Allow other modules to make changes Chris@0: // to the batch so that it is easier to reuse batch processes in other Chris@0: // environments. Chris@0: \Drupal::moduleHandler()->alter('batch', $batch); Chris@0: Chris@0: // Assign an arbitrary id: don't rely on a serial column in the 'batch' Chris@0: // table, since non-progressive batches skip database storage completely. Chris@18: $batch['id'] = \Drupal::database()->nextId(); Chris@0: Chris@0: // Move operations to a job queue. Non-progressive batches will use a Chris@0: // memory-based queue. Chris@0: foreach ($batch['sets'] as $key => $batch_set) { Chris@0: _batch_populate_queue($batch, $key); Chris@0: } Chris@0: Chris@0: // Initiate processing. Chris@0: if ($batch['progressive']) { Chris@0: // Now that we have a batch id, we can generate the redirection link in Chris@0: // the generic error message. Chris@0: /** @var \Drupal\Core\Url $batch_url */ Chris@0: $batch_url = $batch['url']; Chris@0: /** @var \Drupal\Core\Url $error_url */ Chris@0: $error_url = clone $batch_url; Chris@0: $query_options = $error_url->getOption('query'); Chris@0: $query_options['id'] = $batch['id']; Chris@0: $query_options['op'] = 'finished'; Chris@0: $error_url->setOption('query', $query_options); Chris@0: Chris@0: $batch['error_message'] = t('Please continue to the error page', [':error_url' => $error_url->toString(TRUE)->getGeneratedUrl()]); Chris@0: Chris@0: // Clear the way for the redirection to the batch processing page, by Chris@0: // saving and unsetting the 'destination', if there is any. Chris@0: $request = \Drupal::request(); Chris@0: if ($request->query->has('destination')) { Chris@0: $batch['destination'] = $request->query->get('destination'); Chris@0: $request->query->remove('destination'); Chris@0: } Chris@0: Chris@0: // Store the batch. Chris@0: \Drupal::service('batch.storage')->create($batch); Chris@0: Chris@0: // Set the batch number in the session to guarantee that it will stay alive. Chris@0: $_SESSION['batches'][$batch['id']] = TRUE; Chris@0: Chris@0: // Redirect for processing. Chris@0: $query_options = $error_url->getOption('query'); Chris@0: $query_options['op'] = 'start'; Chris@0: $query_options['id'] = $batch['id']; Chris@0: $batch_url->setOption('query', $query_options); Chris@0: if (($function = $batch['redirect_callback']) && function_exists($function)) { Chris@0: $function($batch_url->toString(), ['query' => $query_options]); Chris@0: } Chris@0: else { Chris@0: return new RedirectResponse($batch_url->setAbsolute()->toString(TRUE)->getGeneratedUrl()); Chris@0: } Chris@0: } Chris@0: else { Chris@0: // Non-progressive execution: bypass the whole progressbar workflow Chris@0: // and execute the batch in one pass. Chris@0: require_once __DIR__ . '/batch.inc'; Chris@0: _batch_process(); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Retrieves the current batch. Chris@0: */ Chris@0: function &batch_get() { Chris@0: // Not drupal_static(), because Batch API operates at a lower level than most Chris@0: // use-cases for resetting static variables, and we specifically do not want a Chris@0: // global drupal_static_reset() resetting the batch information. Functions Chris@0: // that are part of the Batch API and need to reset the batch information may Chris@0: // call batch_get() and manipulate the result by reference. Functions that are Chris@0: // not part of the Batch API can also do this, but shouldn't. Chris@0: static $batch = []; Chris@0: return $batch; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Populates a job queue with the operations of a batch set. Chris@0: * Chris@0: * Depending on whether the batch is progressive or not, the Chris@0: * Drupal\Core\Queue\Batch or Drupal\Core\Queue\BatchMemory handler classes will Chris@0: * be used. The name and class of the queue are added by reference to the Chris@0: * batch set. Chris@0: * Chris@0: * @param $batch Chris@0: * The batch array. Chris@0: * @param $set_id Chris@0: * The id of the set to process. Chris@0: */ Chris@0: function _batch_populate_queue(&$batch, $set_id) { Chris@0: $batch_set = &$batch['sets'][$set_id]; Chris@0: Chris@0: if (isset($batch_set['operations'])) { Chris@0: $batch_set += [ Chris@0: 'queue' => [ Chris@0: 'name' => 'drupal_batch:' . $batch['id'] . ':' . $set_id, Chris@0: 'class' => $batch['progressive'] ? 'Drupal\Core\Queue\Batch' : 'Drupal\Core\Queue\BatchMemory', Chris@0: ], Chris@0: ]; Chris@0: Chris@0: $queue = _batch_queue($batch_set); Chris@0: $queue->createQueue(); Chris@0: foreach ($batch_set['operations'] as $operation) { Chris@0: $queue->createItem($operation); Chris@0: } Chris@0: Chris@0: unset($batch_set['operations']); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns a queue object for a batch set. Chris@0: * Chris@0: * @param $batch_set Chris@0: * The batch set. Chris@0: * Chris@0: * @return Chris@0: * The queue object. Chris@0: */ Chris@0: function _batch_queue($batch_set) { Chris@0: static $queues; Chris@0: Chris@0: if (!isset($queues)) { Chris@0: $queues = []; Chris@0: } Chris@0: Chris@0: if (isset($batch_set['queue'])) { Chris@0: $name = $batch_set['queue']['name']; Chris@0: $class = $batch_set['queue']['class']; Chris@0: Chris@0: if (!isset($queues[$class][$name])) { Chris@0: $queues[$class][$name] = new $class($name, \Drupal::database()); Chris@0: } Chris@0: return $queues[$class][$name]; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * @} End of "defgroup batch". Chris@0: */