Score:0

How to add/install a new field to all bundles of certain entity types?

sa flag

I need to add a new string field called instagram_owner to all bundles of nodes, media and file entities. If new bundles are created, I need the field on them too. This is what I have so far in my MODULE_NAME.module file.

use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Entity\EntityTypeBundleInfo;
use Drupal\Core\Entity\EntityFieldManager;

/**
 * Implements hook_entity_base_field_info().
 */
function MODULE_NAME_entity_base_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) {
    $entity_type_id = $entity_type->id();
    $field_name = 'instagram_owner';
    
    // Check for the entity types
    if ($entity_type_id === 'node' || $entity_type_id === 'media' || $entity_type_id === 'file') { 
        
        // Specify new Instagram owner field
        $fields[$field_name] = BaseFieldDefinition::create('string')
            ->setLabel(new TranslatableMarkup("Instagram owner id"))
            ->setDescription(new TranslatableMarkup("This is the owners Instagram ID"))
            ->setSettings(["max_length" => 255, "text_processing" => 0])
            ->setDefaultValue("")
            ->setDisplayOptions("view", ["label" => "above", "type" => "string", "weight" => -3])
            ->setDisplayOptions("form", ["type" => "string_textfield", "weight" => -3]);
        
        // Get the bundles for this entity type
        $bundle_service = \Drupal::service('entity_type.bundle.info');
        $bundles = $bundle_service->getBundleInfo($entity_type_id);
        
        foreach($bundles as $bundle_id => $bundle) {
            
            // Check to see if our field already exists on this bundle
            $bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle_id);
            if (!isset($bundle_fields[$field_name])) {
                
                // If not, install it...
                // not sure if this will install the field either? 
                // Where do I get $storage_definition from? This has never been uncommented.
                // BaseFieldDefinition::installFieldStorageDefinition($field_name, $entity_type_id, $bundle_id, FieldStorageDefinitionInterface $storage_definition);
                
            }
        }
        return $fields;
    }
    
}

I know that the problematic line seems to be this one. It's this that throws a 503 error page:

$bundle_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle_id);

In devels php block I get the expected results. I'm using this for testing:

    $entity_type_id = 'node';
    $field_name = 'instagram_owner';

    if ($entity_type_id === 'node' || $entity_type_id === 'media' || $entity_type_id === 'file') {        
                
        $bundle_service = \Drupal::service('entity_type.bundle.info');
        $bundles = $bundle_service->getBundleInfo($entity_type_id);
        dpm($bundles);
        $field_service = \Drupal::service('entity_field.manager');
        foreach($bundles as $bundle_id => $bundle) {
            $bundle_fields = $field_service->getFieldDefinitions($entity_type_id, $bundle_id);
            if (!isset($bundle_fields[$field_name])) {
                
                $ready_to_install = 'yes';
                dpm($ready_to_install);
                
            }
        }
        dpm($bundle_fields);
    }

Any help would be greatly appreciated.

4uk4 avatar
cn flag
The error is caused by a recursive call getting field definitions while building field definitions. For the overall goal, I don't know if this is a good way to resolve a conflict between an existing bundle fields and the new base field. If there are already existing bundle fields I would add the same bundle field to the other bundles as well. You can re-use the field storage.
gMaximus avatar
sa flag
@4uk4 Have you got any tips regarding how you would approach this? When I remove the foreach loop, the field appears as expected but no schema for storage. The foreach was to install the fields on the bundles that don't have it. Am I even on the right track here?
Score:0
cn flag

Field storage is not bundle specific. The easiest way is to install the field storage via a config yml file in the module config install folder and then add a bundle field to each existing or later added bundle, like core is doing it for the body field. See Add content type field programmatically

Or remove any existing bundle field with that field name and add a non-bundle-specific base field for each entity type in the hook_entity_base_field_info() you are already using.

I would recommend the first way via config, this is in general a more reliable way to install and uninstall fields to existing entities.

gMaximus avatar
sa flag
Apologies, my dog of 13 years died unexpectedly. Pretty tough. In the example given, the function is node_add_body_field. I'm working on instagram_feeds whilst the field is instagram_owner. Does that mean I should call mine instagram_feeds_add_instagram_owner_field? If this isn't a hook, how does this code get invoked? I'm fairly new from D7. This example is specific to content types. How would I achieve the same for the other entity types? Do I need separate field storage definitions for each of the three entity types I'm interested in? I really appreciate your time helping.
4uk4 avatar
cn flag
This is called in the content type submit handler [NodeTypeForm::save](https://api.drupal.org/api/drupal/core%21modules%21node%21src%21NodeTypeForm.php/function/NodeTypeForm%3A%3Asave). You could run this code also in a hook_entity_insert() for the bundle of the entity types, not for the entity types themselve (the core File entity has no bundles by the way).
Score:0
sa flag

There were a few pieces of the puzzle I hadn't understood. Firstly, hook_entity_base_field_info() just defines the specification of the base field. I originally thought I also had to install the field on each bundle within that hook. Not true.

Once the field was defined in this hook, the field appears on every bundle because it is a base field. However values won't actually get saved.

So the next thing I couldn't get my head around was how to install the schema to store the values. I learned that this can be taken care of with hook_install, hook_uninstall and if needed hook_update_N.

So, 3 days of pain later, this is what I ended up with:

my_module.module

/**
 * Implements hook_entity_base_field_info().
 */
function MY_MODULE_entity_base_field_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) {
    
    // Only install on nodes and media.
    $entity_type_id = $entity_type->id();
    if ($entity_type_id === 'node' || $entity_type_id === 'media') { 
        if ($entity_type_id === 'node') {
            $field_name = 'instagram_owner_display';
        }
        elseif ($entity_type_id === 'media') {
            $field_name = 'instagram_owner_media';
        }
        
        // Specify new Instagram owner field
        $fields[$field_name] = BaseFieldDefinition::create('string')
            ->setLabel(new TranslatableMarkup("Instagram owner ID"))
            ->setDescription(new TranslatableMarkup("This is the Instagram Account ID of who imported this."))
            ->setSettings(["max_length" => 255, "text_processing" => 0])
            ->setDefaultValue("")
            ->setDisplayOptions("view", ["label" => "above", "type" => "string"])
            ->setDisplayOptions("form", ["type" => "string_textfield"]);
        return $fields;
    }
}

my_module.install

/**
 * Implements hook_install().
 */
function MY_MODULE_install() {
    
    // Install Instagram owner field on nodes and media
    $field_manager = \Drupal::service('entity_field.manager'); 
    $field_storage_manager = \Drupal::service('field_storage_definition.listener'); 
    
    $node_definition = $field_manager->getFieldStorageDefinitions('node')['instagram_owner_display']; 
    $field_storage_manager->onFieldStorageDefinitionCreate($node_definition);
    
    $media_definition = $field_manager->getFieldStorageDefinitions('media')['instagram_owner_media'];
    $field_storage_manager->onFieldStorageDefinitionCreate($media_definition);
}

/**
 * Implements hook_uninstall().
 */
function MY_MODULE_uninstall() {
    
    // Delete Instagram owner field on nodes and media
    $field_manager = \Drupal::service('entity_field.manager'); 
    $field_storage_manager = \Drupal::service('field_storage_definition.listener'); 
    
    $node_definition = $field_manager->getFieldStorageDefinitions('node')['instagram_owner_display']; 
    $field_storage_manager->onFieldStorageDefinitionDelete($node_definition);
    
    $media_definition = $field_manager->getFieldStorageDefinitions('media')['instagram_owner_media'];
    $field_storage_manager->onFieldStorageDefinitionDelete($media_definition);
}

/**
 * Install the new Instagram owner field on existing media and node bundles.
 */
function MY_MODULE_update_N() {
    
    // Install Instagram owner field on nodes and media
    $field_manager = \Drupal::service('entity_field.manager'); 
    $field_storage_manager = \Drupal::service('field_storage_definition.listener'); 
    
    $node_definition = $field_manager->getFieldStorageDefinitions('node')['instagram_owner_display']; 
    $field_storage_manager->onFieldStorageDefinitionCreate($node_definition);
    
    $media_definition = $field_manager->getFieldStorageDefinitions('media')['instagram_owner_media'];
    $field_storage_manager->onFieldStorageDefinitionCreate($media_definition);
}
I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.