I'm not a fan of the summary option on body fields in Drupal. I've never really got on with how users interact with it or the content is produces.
The field type I'm talking about is called "Text (formatted, long, with summary)" and it appears as an add-on summary field to a normal content editor area. It comes as standard on Drupal installs and appears on all body fields. The field type has it's uses, but I often find that the content it produces is unpredictable and doesn't have a great editing experience. I have even written articles in the past about swapping the summary field to a fully fledged wysiwyg area for Drupal 7, which worked on the project I implemented it on.
Let's look at the body summary field in Drupal and see why I have a few problems with it.
The Body Summary
If you create a new content type in Drupal you will automatically get a body field. This field will always be a "Text (formatted, long, with summary) and will have a machine name of "body". This is unlike most other fields in Drupal which get a prefix of "field_" to the field machine name. You can also create your own fields with of the same type as the default body field.
The field footprint looks like this in the field management page of the content type.
When editing or creating a page of content the body field is shown like this.
Clicking on "Edit summary" will show the summary of the field, allowing users to enter text into it.
The problem is that if you enter content into the summary it doesn't get treated as markup because the summary isn't a markup field. Users with the ability to do so can enter raw HTML into the summary, but it will just be stripped out as the field is passed through the standard twig output filters. You can allow users to bypass this, but that poses a security risk as no input filter is applied to the output at all.
If you don't enter content into the summary field it then the beginning of the body field is used as the summary, with the markup intact. This can be controlled through the break element, but that feature isn't commonly used on most Drupal sites I have seen in the last few years.
This mix of content makes the summary unpredictable and difficult to work with. You can get around some of these inconsistencies by simply wrapping the summary field in paragraph tags when it is rendered on the front end, but this creates a unique situation where the summary doesn't act in the same way as normal fields. Users will often want to add markup to the summary since the body field contains that markup and they will expect to be able to add links or bold tags to it. The body summary field will quite often be the focus of support requests to allow it to produce HTML output or even be a normal wysiwyg area.
A much better way of capturing the summary is to create a secondary field called Summary that is a "Text (formatted, long)" field type. This allows users to enter rich content into both the body field and the summary field and is a standard formatted text field (without any summary option). The same field (with the same machine name) can be found in the Umami demo install profile.
With this new Summary field in place we also need to setup the content display so that the summary field is displayed in full on the teaser display mode. Since we don't want the summary field to be too long we can also include the Drupal Maxlength module that will restrict the amount of characters that can be entered into the content. We now have much better control over what is considered the summary of the page and it is much more predictable.
We also need to prevent users from entering copy into the body summary, since that copy will never be presented anywhere. Let's look at a couple of ways to remove the summary from the body field.
Solution 1: Turn Off The Summary
The simple solution here is to just turn off the summary input. This is easily achieved in Drupal via a checkbox on the field configuration screen, found by clicking the 'edit' button next to the field description on the field management page of the content type.
This is what that option looks like.
Turning this Summary input setting off prevents users from seeing the summary field and entering content into it as the option to show the summary is no longer present. The database table for the body will still contain the summary field and any data already entered into it is now locked since users can't access the summary input.
The edit page of the content type we changed now looks like this. The body and summary fields are separate fields and we have now turned off the body summary entry.
Solution 2: Completely Remove The Body Summary
Whilst turning that setting off is fine, I wanted to take this to another level and completely remove the summary from the body field. This essentially means changing the type of field that the body field is defined as, therefore removing the summary option from the field and database table. Finding out how to do this took me down a bit of a rabbit hole in the Drupal field and configuration API, but it turns out that it is perfectly possible.
What I also wanted to do was to take any content that was already in the body summary and copy it into the new Summary field.
I should warn you before going any further that this code can be dangerous! In other words, here be dragons! Running this code without fully understanding it can potentially butcher your site configuration and cause your site to crash and be rendered useless. If you do want to run this code then make sure you have database backups before entering into this process in case anything goes wrong. Changing the type of field is not something to be done lightly, but I was sure that this is something that was needed in order to have the body and summary fields as separate, fully controllable, fields with no option of adding in a summary from the body field.
Let's go through each step one at a time. There is a little bit of code to do through here and the idea is that this is run in an update hook, which I will detail in full at the end.
1) Remove the summary field from the body field storage definition. This setting is tells Drupal what shape the database table for the body field is so by removing the summary field we are effectively changing the shape of the table that Drupal can see. It is kept in the Drupal key/value store so we just fetch it out of there, remove the summary from the table definition, and add the setting back into the key/value storage.
# Load the field schema definition and remove the summary.
$bodyStorageDefinition = \Drupal::keyValue('entity.storage_schema.sql')->get('node.field_schema_data.body');
unset($bodyStorageDefinition['node__body']['fields']['body_summary']);
unset($bodyStorageDefinition['node_revision__body']['fields']['body_summary']);
\Drupal::keyValue('entity.storage_schema.sql')->set('node.field_schema_data.body', $bodyStorageDefinition);
2) Next we need to update the field storage definition to have a type of "text_long" instead of "text_long_summary". This setting is used to tell Drupal what the field looks like when creating new content types so without changing this setting you would end up creating new content types with the summary field. This setting is also kept in the Drupal key/value store, but there is a dedicated API that allows access to change and update this setting.
If you use the key/value API to get this setting it is unserialised into an array of objects and it is not possible to change the type property of the FieldConfig object that stores the body field definition. Using the entity definition update manager perhaps the only way to do this, but is also considered the best practice approach to changing field storage definitions.
# Update the field storage definition.
$manager = \Drupal::entityDefinitionUpdateManager();
$body_storage = $manager->getFieldStorageDefinition('body', 'node');
$new_body_storage = $body_storage->toArray();
$new_body_storage['type'] = 'text_long';
$new_body_storage = \Drupal\field\Entity\FieldStorageConfig::create($new_body_storage);
$new_body_storage->original = $new_body_storage;
$new_body_storage->enforceIsNew(FALSE);
$new_body_storage->save();
3) Set the body field to storage configuration to have a field type of "text_long". This setting is used to connect the body field to the storage system and is closely linked with the entity.storage_schema.sql setting above.
# Set the body field to have a field type of 'text_long', without the summary field.
$config = \Drupal::service('config.factory')->getEditable('field.storage.node.body');
$config->set('type', 'text_long');
$config->save();
4) It is also quite important is to change the field instance setting for every content type that contains a body field. This setting tells Drupal to load the body field as a "text_long" field when creating or editing content. We change this setting by loading the field instance configuration out of Drupal, changing the field type and then saving it. This could be further automated by loading in a list of all of the content types, but I have instead just hard coded the standard Article and Page content types.
# Set the body field instance on the content types that contain the body field to have a type of 'text_long'.
foreach (['article', 'page'] as $content_type) {
$config = \Drupal::service('config.factory')->getEditable('field.field.node.' . $content_type . '.body');
$config->set('field_type', 'text_long')->save();
}
5) With the field definition sorted out we now need to copy the content from the body field into the new summary field. This will populate the summary field in the site with content from the body field and mean that there are no gaps in the content. The simplest (and quickest) way to do this is to pull the available data out of the body field using a the database and simply insert it into the relevant data storage tables. There is some logic here to look at the value of the body field and either use the current summary or generate a new one on the fly from the existing markup.
# Load all of the data from the body field.
$database = \Drupal::database();
$result = $database->query("SELECT * FROM {node__body};");
if ($result) {
# Copy the body_summary field data into the new field_summary value.
while ($row = $result->fetchAssoc()) {
if (is_null($row['body_summary']) || trim($row['body_summary']) == '') {
# There is no summary so create one fom the body copy.
$summary = text_summary($row['body_value'], $row['body_format'], 1000);
} else {
# A summary has been set, so surround it with usable markup.
$summary = '<p>' . $row['body_summary'] . '</p>';
}
$placeholders = [
':bundle' => $row['bundle'],
':deleted' => $row['deleted'],
':entity_id' => $row['entity_id'],
':revision_id' => $row['revision_id'],
':langcode' => $row['langcode'],
':delta' => $row['delta'],
':field_summary_value' => $summary,
':field_summary_format' => $row['body_format'],
];
$database->query('INSERT INTO {node__field_summary}
VALUES (:bundle, :deleted, :entity_id, :revision_id, :langcode, :delta, :field_summary_value, :field_summary_format)', $placeholders);
$database->query('INSERT INTO {node_revision__field_summary}
VALUES (:bundle, :deleted, :entity_id, :revision_id, :langcode, :delta, :field_summary_value, :field_summary_format)', $placeholders);
}
}
If you have a lot of content on your site then you might want to run this as a batch process as doing so will prevent the code from timing out if there are lots of data to process. This process only took a few seconds on an existing Drupal site with around 800 items of content so it is quite quick.
After running all that code we are left with a field definition that looks like this (without the summary field).
Any new content types that we create will automatically get the same field definition of a body field without the summary option.
The final piece in all of this is the deployment. There's a sort of chicken and egg situation between the new configuration of the site and the update hook we are trying to run. We can't write data to a summary field table that doesn't exist, so we can't run the update hook before importing config. We also can't import the config and create the new Summary field table correctly as it the new body field definition causes a configuration import error. It isn't possible to change the field storage type in the configuration so Drupal will just error when it sees that change.
My normal deployment process runs the update hooks first before importing config, so the update hook therefore needs to create the summary field tables before we can put data into it. I have written about adding config to Drupal in an update hook before, but this is slightly different since it's a field that we need to import from configuration. If we just import it using the \Drupal::service('config.storage') service then the tables are not created so we need an alternative method that will set up the fields correctly.
We can use the entity type manager service from Drupal to create the field storage and instance configuration (and by extension the tables needed for the field) on the fly. The following code will read the new Summary field configuration from the configuration files and write it into Drupal. Doing it this way ensures that the correct tables and configuration is setup for the rest of the code to run.
$config_path = realpath('../config/sync');
$source = new FileStorage($config_path);
// Import the field_summary storage config.
\Drupal::entityTypeManager()->getStorage('field_storage_config')
->create($source->read('field.storage.node.field_summary'))
->save();
// Import the instance config for the field_summary field.
\Drupal::entityTypeManager()->getStorage('field_config')
->create($source->read('field.field.node.article.field_summary'))
->save();
Here is the full update hook with all of the needed code added together. This will create the new Summary field, delete the summary option from the body field and populate the new field with content from the body.
/**
* Transfer body field into body without summary option.
*/
function mymodule_update_9001()
{
$config_path = realpath('../config/sync');
$source = new FileStorage($config_path);
// Import the field_summary storage config.
\Drupal::entityTypeManager()->getStorage('field_storage_config')
->create($source->read('field.storage.node.field_summary'))
->save();
// Import the instance config for the field_summary field.
\Drupal::entityTypeManager()->getStorage('field_config')
->create($source->read('field.field.node.article.field_summary'))
->save();
# Load the field schema definition and remove the summary.
$bodyStorageDefinition = \Drupal::keyValue('entity.storage_schema.sql')->get('node.field_schema_data.body');
unset($bodyStorageDefinition['node__body']['fields']['body_summary']);
unset($bodyStorageDefinition['node_revision__body']['fields']['body_summary']);
\Drupal::keyValue('entity.storage_schema.sql')->set('node.field_schema_data.body', $bodyStorageDefinition);
# Update the field storage definition.
$manager = \Drupal::entityDefinitionUpdateManager();
$body_storage = $manager->getFieldStorageDefinition('body', 'node');
$new_body_storage = $body_storage->toArray();
$new_body_storage['type'] = 'text_long';
$new_body_storage = \Drupal\field\Entity\FieldStorageConfig::create($new_body_storage);
$new_body_storage->original = $new_body_storage;
$new_body_storage->enforceIsNew(FALSE);
$new_body_storage->save();
# Set the body field to have a field type of 'text_long', without the summary field.
$config = \Drupal::service('config.factory')->getEditable('field.storage.node.body');
$config->set('type', 'text_long');
$config->save();
# Set the body field instance on the content types that contain the body field to have a type of 'text_long'.
foreach (['article', 'page'] as $content_type) {
$config = \Drupal::service('config.factory')->getEditable('field.field.node.' . $content_type . '.body');
$config->set('field_type', 'text_long')->save();
}
# Load all of the data from the body field.
$database = \Drupal::database();
$result = $database->query("SELECT * FROM {node__body};");
if ($result) {
# Copy the body_summary field data into the new field_summary value.
while ($row = $result->fetchAssoc()) {
if (is_null($row['body_summary']) || trim($row['body_summary']) == '') {
# There is no summary so create one fom the body copy.
$summary = text_summary($row['body_value'], $row['body_format'], 1000);
} else {
# A summary has been set, so surround it with usable markup.
$summary = '<p>' . $row['body_summary'] . '</p>';
}
$placeholders = [
':bundle' => $row['bundle'],
':deleted' => $row['deleted'],
':entity_id' => $row['entity_id'],
':revision_id' => $row['revision_id'],
':langcode' => $row['langcode'],
':delta' => $row['delta'],
':field_summary_value' => $summary,
':field_summary_format' => $row['body_format'],
];
$database->query('INSERT INTO {node__field_summary}
VALUES (:bundle, :deleted, :entity_id, :revision_id, :langcode, :delta, :field_summary_value, :field_summary_format)', $placeholders);
$database->query('INSERT INTO {node_revision__field_summary}
VALUES (:bundle, :deleted, :entity_id, :revision_id, :langcode, :delta, :field_summary_value, :field_summary_format)', $placeholders);
}
}
}
Finally, since all the data has been moved around we can now delete the original summary field. I've created this as a separate update as the database still contains all of the needed data to get back to the old field type (if needed). As Drupal doesn't know about this field it won't have any problems writing new records to the tables, so leaving it in won't have any negative side effects. By removing the field from the table we essentially have no way to get back so this is more of a destructive action.
function mymodule_update_9002() {
// Drop the body_summary field from the body and revision tables.
$database = \Drupal::database();
$database->query('ALTER TABLE `node__body` DROP `body_summary`');
$database->query('ALTER TABLE `node_revision__body` DROP `body_summary`');
}
That is essentially it. Through a couple of update hooks the site now has no record of the old body summary and the users are now able to enjoy a rich content area for the summary field. The final touches to this would be to add some descriptive help text to the two fields to help users know where that content will appear and to use the Maxlength module to prevent the new summary field from being too long.
How do you feel about the body summary field? Have you encountered any issues with it in the past? Let us know in the comments.
Comments
Hi Phil,
Great article, and I agree with you fully. Just 2 things:
1. We don't use "field_summary" in Umami for the purposes you describe above. In Umami, field_summary is the "intro" field we have on the recipe content type. It's not actually used for teasers or anything like that. We only have it as we do not have _any_ body field for that content type. All of the other content types have the Drupal core body field (formatted, with summary). I'd like to see Umami adopting your approach, but we are not currently doing it.
2. I'd encourage against using a formatted field for the summary/teaser field and encourage a long, plain textfield/textarea instead. If users can put html into the teaser field (e.g. a link in the intro for some reason), this can have unintended consequences. For example, if you wanted to put a link around the full teaser to make it all clickable. In that case, you might try click on the link in the text, but wonder why you are being brought to the full view mode of the node rather than the link you clicked on.
Mark
- Drupal core maintainer for Umami.
Submitted by Mark Conroy on Tue, 11/23/2021 - 18:42
PermalinkHi Mark,
Thanks very much for commenting! I really appreciate your thoughts.
1) You are right that the summary field is being used for the introduction in Umami recipes. I only spotted the field because I was searching for my summary field in the config and accidentally found the Umami version too. I thought I would add a reference to it in case people wanted to see a similar field (albeit with a slightly different function).
2) You are right. I hadn't thought about that consequence of allowing full HTML content to be added to the summary. I guess my article would include the caveat "where appropriate" when using the summary field throughout the site :)
Thanks again!
Phil
Submitted by giHlZp8M8D on Tue, 11/23/2021 - 21:28
PermalinkAdd new comment