Paragraphs
Introduction
Paragraphs are those special things that allow you to blend fields together e.g. count and unit of measure so you can store values like 5 kilograms or 7 years, etc. Often, they are used like nodes where you define the fields and fill them with data that gets displayed on the screen for things like carousels or events.
Load a node and find the terms referenced in a paragraph in a term reference field
Here we loop thru all the instances of my paragraph reference and grab the term in the paragraph.
use Drupal\taxonomy\Entity\Term;
foreach ($node->get('field_my_para')->referencedEntities() as $ent){
$term = Term::load($ent->$field_in_paragraph->target_id);
$name = $term->getName();
print_r($name);
}Find all the long text fields that have a certain string
In this code snippet from an issue in the entity embed module the author loads all the fields of type string_long, text_long, and text_with_summary and then queries for nodes with the value data-entity-embed-display-settings="". The code then removes the string and saves the node. It is a useful example because it shows how to find all the fields of a certain type.
Please note that I have not tested this code so it may not be perfect.
<?php
/**
* Example batch update hook.
*/
function just_an_example(&$sandbox) {
// Get the entity type manager and node storage.
$entity_type_manager = \Drupal::entityTypeManager();
$node_storage = $entity_type_manager->getStorage('node');
$limit = 100;
// Initialize the sandbox if it's the first run.
if (!isset($sandbox['total'])) {
// Load all fields of type string_long, text_long, and text_with_summary.
$string_long_fields = array_map(fn ($field) => $field->getName(),
$entity_type_manager
->getStorage('field_storage_config')
->loadByProperties([
'entity_type' => 'node',
'type' => 'string_long',
])
);
$text_long_fields = array_map(fn ($field) => $field->getName(),
$entity_type_manager
->getStorage('field_storage_config')
->loadByProperties([
'entity_type' => 'node',
'type' => 'text_long',
])
);
$text_with_summary_fields = array_map(fn ($field) => $field->getName(),
$entity_type_manager
->getStorage('field_storage_config')
->loadByProperties([
'entity_type' => 'node',
'type' => 'text_with_summary',
])
);
// Merge all field names into a single array.
$sandbox['fields'] = array_merge($string_long_fields, $text_long_fields, $text_with_summary_fields);
// Initialize bundles array.
$sandbox['bundles'] = [];
foreach ($sandbox['fields'] as $field_name) {
// Load field storage config and get bundles for each field.
$field_storage = $entity_type_manager->getStorage('field_storage_config')->load($field_name);
if ($field_storage) {
$sandbox['bundles'] = array_merge($sandbox['bundles'], $field_storage->getBundles());
}
}
// Initialize counters.
$sandbox['current'] = $sandbox['count'] = $sandbox['changed'] = 0;
// Initialize node IDs array.
$sandbox['nids'] = [];
foreach ($sandbox['fields'] as $field) {
// Query for nodes with specific field values and add to node IDs array.
$query = \Drupal::database()->select("node__{$field}", 'f')
->fields('f', ['entity_id'])
->condition("f.{$field}_value", 'data-entity-embed-display-settings=""', 'REGEXP BINARY')
->condition('f.bundle', $sandbox['bundles'], 'IN');
$sandbox['nids'] = array_merge($sandbox['nids'], $query->execute()->fetchCol());
}
// Remove duplicate node IDs and sort them.
$sandbox['nids'] = array_unique($sandbox['nids']);
sort($sandbox['nids']);
$sandbox['total'] = count($sandbox['nids']);
}
// Query for nodes to process in the current batch.
$nids = $node_storage->getQuery()
->accessCheck(FALSE)
->range(0, $limit)
->condition('nid', $sandbox['nids'], 'IN')
->condition('nid', $sandbox['current'], '>=')
->sort('nid', 'ASC');
$nodes = $nids->execute();
$nodes = $node_storage->loadMultiple($nodes);
// Process each node.
foreach ($nodes as $node) {
$languages = $node->getTranslationLanguages();
// Process each language translation of the node.
foreach ($languages as $langcode => $lang_obj) {
$cs_node = $node->getTranslation($langcode);
$changed_node = FALSE;
// Process each field in the node.
foreach ($sandbox['fields'] as $field_name) {
if ($cs_node->hasField($field_name)) {
$value = $cs_node->get($field_name)->getValue();
$changed_field = FALSE;
// Check and update field values.
foreach ($value as &$content) {
if (preg_match_all('/data-entity-embed-display-settings=""/', $content['value'], $matches)) {
foreach ($matches[0] as $replacement) {
// Remove the match and flag for saving.
$content['value'] = str_replace($replacement, '', $content['value']);
$changed_field = $changed_node = TRUE;
}
}
}
// Save the updated field value.
if ($changed_field) {
$cs_node->set($field_name, $value);
}
}
}
// Save the node if any field was changed.
if ($changed_node) {
$sandbox['changed']++;
$cs_node->save();
}
}
// Update the current node ID and increment the count.
$sandbox['current'] = $node->id();
$sandbox['count']++;
}
// Update the progress and display status messages.
$sandbox['#finished'] = empty($sandbox['total']) ? 1 : ($sandbox['count'] / $sandbox['total']);
\Drupal::messenger()->addStatus($sandbox['count'] . ' nodes processed out of ' . $sandbox['total']);
\Drupal::messenger()->addStatus($sandbox['changed'] . ' nodes altered');
}Load a node and retrieve a paragraph field
Because paragraphs and nodes are both entities, the pattern is the same. You load the entity (node or paragraph) and then simply reference the field name e.g. myentity->field_blah
From /Users/selwyn/Sites/inside-mathematics/themes/custom/danaprime/danaprime.theme
These are a little different from regular fields. Generally, you want to get their target_id which will tell you the pid or paragraph id. Here are two different ways to load a video_collection_node and go to retrieve a field field_related_lessons which holds paragraphs of type related_lessons:
$video_collection_node = Node::load($video_collection_nid);
//This gives you a bunch of \Drupal\entity_reference_revisions\EntityReferenceRevisionsFieldItemList items
$lessons = $video_collection_node->field_related_lessons;
//or
$lessons = $video_collection_node->get('field_related_lessons');
foreach ($lessons as $lesson) {
$paragraph_revision_ids[] = $lesson->target_revision_id;
}Paragraphs use the contributed Entity Reference Revisions module to reference paragraphs and it is very important to use the target_revision_id property when referencing paragraphs. Alternatively, the entity computed property can be used to retrieve the paragraph entity itself.
OR
//This gives you an array of arrays [['target_id' => '348','target_revision_id' => '348'],['target_id' => '349','target_revision_id' => '349'] ]
$lessons = $video_collection_node->get('field_related_lessons')->getValue();
foreach ($lessons as $lesson) {
$paragraph_revision_ids[] = $lesson['target_revision_id'];
}Collecting them like this is only an example, while the loadMultiple method exists on entity storage objects, there is no loadMultipleRevisions method.
// This gives you null! - don't do this.
$lessons = $video_collection_node->get('field_related_lessons')->value;Note
Note, getValue() here will get you the nid buried in a result array of arrays like result[0]['target_revision_id'] - quicker to just grab ->target_revision_id
Load a node and grab a paragraph field to find the nid in an entity reference field
From /Users/selwyn/Sites/inside-mathematics/themes/custom/danaprime/danaprime.theme - Continuing from above, I load a node, grab its field field_related_lessons which holds paragraphs of type related_lessons and grab its field field_lesson. That field has a target_id which is the nid for the entity reference field. Phew!:
//Grab the related lessons from the collection.
$video_collection_node = Node::load($video_collection_nid);
$lessons = $video_collection_node->field_related_lessons;
$storage = \Drupal::entityTypeManager()->getStorage('paragraph');
foreach ($lessons as $lesson) {
//Load each paragraph and get the nids from them.
$paragraph = $lesson->entity;
$related_lessons_nid = $paragraph->field_lesson->target_id;
$related_lessons_nids[] = $related_lessons_nid;
}
//This should have an array of nids for video_details.
$variables['related_lessons_pids'] = $pids;
$variables['related_lessons_nids'] = $related_lessons_nids;Loop through the paragraphs in a node
This code is from a hook_preprocess_node function in a .theme file. It loads a node and then loops through the paragraphs in a field field_related_lessons and then grabs the title of the paragraph.
<?php
use Drupal\Core\Render\Element;
/**
* Implements hook_preprocess_node().
*/
function abc_preprocess_node(&$variables) {
/** @var \Drupal\node\Entity\Node $node */
$node = $variables['node'];
$bundle = $node->bundle();
$view_mode = $variables['view_mode'];
// Check that there is a field_content field.
if (isset($variables['content']['field_content'])) {
$content_tabs = [];
// Loop through each instance of the field_content field.
foreach (Element::children($variables['content']['field_content']) as $idx) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
// Grab each paragraph.
$entity = $variables['content']['field_content'][$idx]['#paragraph'];
// Get the paragraph's human-readable type (not machine name).
$bundle_label = \Drupal::entityTypeManager()
->getStorage($entity->getEntityType()->getBundleEntityType())
->load($entity->bundle())
->label();
// For staff_profile content, strip off the "Staff Profile" prefix.
if ($bundle === 'staff_profile') {
// This makes "Staff Profile Staff Resources" into "Resources".
$bundle_label = preg_replace('#^(Staff Profile )#i', '', $bundle_label);
$variables['offset'] = 0;
// Check if there is a title term on the node.
if (($title_term = $node->get('field_staff_profile_title')->entity) !== NULL) {
// If the title term has a field_global field and it is set to 1, offset the first tab.
if ($title_term->hasField('field_global') && $title_term->get('field_global')->getString() == '1') {
$variables['offset'] = 1;
}
}
}
...Add validation for paragraph fields
Here, we want to perform validation when a node is added or edited so we use hook_form_alter to add a validation function to the form. In this case, we are adding a validation function to a node form with the id node_staff_profile_form and node_staff_profile_edit_form. We add a class to the form so we can target it with CSS and then add a validation function _ccr_admin_enhancements_area_of_expertise_validate to the form.
Note
The validate function fires at various times, including when a paragraph is added, removed or closed. We check for these conditions and then perform our validations.
In abc_admin_enhancements.module:
/**
* Implements hook_form_alter().
*/
function ccr_admin_enhancements_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if ($form_id === 'node_staff_profile_form' || $form_id === 'node_staff_profile_edit_form') {
$form['#attributes']['class'][] = 'staff-profile-form';
$form['#validate'][] = '_abc_admin_enhancements_area_of_expertise_validate';
}
}
/**
* Validate the area of expertise field.
*/
function _abc_admin_enhancements_area_of_expertise_validate(&$form, FormStateInterface &$form_state) {
// Don't perform validations if the field doesn't exist.
if (!($form_state->hasValue('field_areas_of_expertise'))) {
return;
}
if ($form_id === 'node_staff_profile_form' || $form_id === 'node_staff_profile_edit_form') {
$form['#attributes']['class'][] = 'staff-profile-form';
$form['#validate'][] = '_ccr_admin_enhancements_area_of_expertise_validate';
/*
* Special primary titles: Assistant Research Physician, Associate Research
* Physician, Senior Research Physician, OR Staff Clinician (Contr).
*/
$special_primary_titles = [2477, 2478, 2558, 2651];
$profile_type = $form_state->getValue('field_staff_profile_type')[0]['target_id'] ?? NULL;
$ccr_primary_title = $form_state->getValue('field_staff_profile_title')[0]['target_id'] ?? NULL;
// Some validations will not apply to Other Staff profiles.
$is_other_staff = !empty($profile_type) && $profile_type == 2575;
$button = $form_state->getTriggeringElement();
// Check what paragraph button was pressed: add, remove or close.
if (!empty($button)) {
if ( isset($button['#paragraphs_mode']) && $button['#paragraphs_mode'] === 'remove') {
// We don't need to validate on paragraph removal.
return;
}
else if ($button['#name'] === 'field_areas_of_expertise_area_of_expertise_add_more') {
// We don't need to validate on paragraph addition.
return;
}
}
$values = $form_state->getValue('field_areas_of_expertise');
// Paragraphs come to us indexed by a number, not necessarily zero-based.
$first = TRUE;
// Loop through each area of expertise paragraph.
foreach ($values as $i => $value) {
// Disease focus is required for the first area of expertise for certain profiles.
if ($first) {
if (empty($disease_focus) && $is_other_staff && in_array($ccr_primary_title, $special_primary_titles)) {
$form_state->setErrorByName('field_areas_of_expertise][' . $i . '][subform][field_disease_focus', 'Area of Expertise: Disease Focus is required for this profile type.');
continue;
}
}
// At least one of these fields must be filled in.
if (empty($custom_text) && empty($research_area) && empty($disease_focus)) {
$form_state->setErrorByName('field_areas_of_expertise][' . $i . '][subform', 'Area of Expertise: One of these options must be selected.');
continue;
}
}
}Load a node and find the terms referenced in a paragraph in a term reference field
Here we loop thru all the instances of my paragraph reference and grab the term in the paragraph.
foreach ($node->get('field_my_para')->referencedEntities() as $ent){
$term = $ent->$field_in_paragraph->entity;
$name = $term->getName();
print_r($name);
}