annotate core/modules/layout_builder/src/QuickEditIntegration.php @ 5:12f9dff5fda9 tip

Update to Drupal core 8.7.1
author Chris Cannam
date Thu, 09 May 2019 15:34:47 +0100
parents
children
rev   line source
Chris@5 1 <?php
Chris@5 2
Chris@5 3 namespace Drupal\layout_builder;
Chris@5 4
Chris@5 5 use Drupal\Component\Utility\NestedArray;
Chris@5 6 use Drupal\Core\Cache\CacheableMetadata;
Chris@5 7 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
Chris@5 8 use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
Chris@5 9 use Drupal\Core\Entity\EntityInterface;
Chris@5 10 use Drupal\Core\Entity\EntityTypeManagerInterface;
Chris@5 11 use Drupal\Core\Entity\FieldableEntityInterface;
Chris@5 12 use Drupal\Core\Logger\LoggerChannelTrait;
Chris@5 13 use Drupal\Core\Plugin\Context\Context;
Chris@5 14 use Drupal\Core\Plugin\Context\ContextDefinition;
Chris@5 15 use Drupal\Core\Plugin\Context\EntityContext;
Chris@5 16 use Drupal\Core\Render\Element;
Chris@5 17 use Drupal\Core\Session\AccountInterface;
Chris@5 18 use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
Chris@5 19 use Symfony\Component\DependencyInjection\ContainerInterface;
Chris@5 20
Chris@5 21 /**
Chris@5 22 * Helper methods for Quick Edit module integration.
Chris@5 23 *
Chris@5 24 * @internal
Chris@5 25 * This is an internal utility class wrapping hook implementations.
Chris@5 26 */
Chris@5 27 class QuickEditIntegration implements ContainerInjectionInterface {
Chris@5 28
Chris@5 29 use LoggerChannelTrait;
Chris@5 30
Chris@5 31 /**
Chris@5 32 * The section storage manager.
Chris@5 33 *
Chris@5 34 * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
Chris@5 35 */
Chris@5 36 protected $sectionStorageManager;
Chris@5 37
Chris@5 38 /**
Chris@5 39 * The current user.
Chris@5 40 *
Chris@5 41 * @var \Drupal\Core\Session\AccountInterface
Chris@5 42 */
Chris@5 43 protected $currentUser;
Chris@5 44
Chris@5 45 /**
Chris@5 46 * The entity type manager.
Chris@5 47 *
Chris@5 48 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
Chris@5 49 */
Chris@5 50 protected $entityTypeManager;
Chris@5 51
Chris@5 52 /**
Chris@5 53 * Constructs a new QuickEditIntegration object.
Chris@5 54 *
Chris@5 55 * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
Chris@5 56 * The section storage manager.
Chris@5 57 * @param \Drupal\Core\Session\AccountInterface $current_user
Chris@5 58 * The current user.
Chris@5 59 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
Chris@5 60 * The entity type manager.
Chris@5 61 */
Chris@5 62 public function __construct(SectionStorageManagerInterface $section_storage_manager, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager) {
Chris@5 63 $this->sectionStorageManager = $section_storage_manager;
Chris@5 64 $this->currentUser = $current_user;
Chris@5 65 $this->entityTypeManager = $entity_type_manager;
Chris@5 66 }
Chris@5 67
Chris@5 68 /**
Chris@5 69 * {@inheritdoc}
Chris@5 70 */
Chris@5 71 public static function create(ContainerInterface $container) {
Chris@5 72 return new static(
Chris@5 73 $container->get('plugin.manager.layout_builder.section_storage'),
Chris@5 74 $container->get('current_user'),
Chris@5 75 $container->get('entity_type.manager')
Chris@5 76 );
Chris@5 77 }
Chris@5 78
Chris@5 79 /**
Chris@5 80 * Alters the entity view build for Quick Edit compatibility.
Chris@5 81 *
Chris@5 82 * When rendering fields outside of normal view modes, Quick Edit requires
Chris@5 83 * that modules identify themselves with a view mode ID in the format
Chris@5 84 * [module_name]-[information the module needs to rerender], as prescribed by
Chris@5 85 * hook_quickedit_render_field().
Chris@5 86 *
Chris@5 87 * @param array $build
Chris@5 88 * The built entity render array.
Chris@5 89 * @param \Drupal\Core\Entity\EntityInterface $entity
Chris@5 90 * The entity.
Chris@5 91 * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
Chris@5 92 * The entity view display.
Chris@5 93 *
Chris@5 94 * @see hook_quickedit_render_field()
Chris@5 95 * @see layout_builder_quickedit_render_field()
Chris@5 96 */
Chris@5 97 public function entityViewAlter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
Chris@5 98 if (!$entity instanceof FieldableEntityInterface || !isset($build['_layout_builder'])) {
Chris@5 99 return;
Chris@5 100 }
Chris@5 101
Chris@5 102 $build['#cache']['contexts'][] = 'user.permissions';
Chris@5 103 if (!$this->currentUser->hasPermission('access in-place editing')) {
Chris@5 104 return;
Chris@5 105 }
Chris@5 106
Chris@5 107 $cacheable_metadata = CacheableMetadata::createFromRenderArray($build);
Chris@5 108 $section_list = $this->sectionStorageManager->findByContext(
Chris@5 109 [
Chris@5 110 'display' => EntityContext::fromEntity($display),
Chris@5 111 'entity' => EntityContext::fromEntity($entity),
Chris@5 112 'view_mode' => new Context(new ContextDefinition('string'), $display->getMode()),
Chris@5 113 ],
Chris@5 114 $cacheable_metadata
Chris@5 115 );
Chris@5 116 $cacheable_metadata->applyTo($build);
Chris@5 117
Chris@5 118 if (empty($section_list)) {
Chris@5 119 return;
Chris@5 120 }
Chris@5 121
Chris@5 122 // Create a hash of the sections and use it in the unique Quick Edit view
Chris@5 123 // mode ID. Any changes to the sections will result in a different hash,
Chris@5 124 // forcing Quick Edit's JavaScript to recognize any changes and retrieve
Chris@5 125 // up-to-date metadata.
Chris@5 126 $sections_hash = hash('sha256', serialize($section_list->getSections()));
Chris@5 127
Chris@5 128 // Track each component by their plugin ID, delta, region, and UUID.
Chris@5 129 $plugin_ids_to_update = [];
Chris@5 130 foreach (Element::children($build['_layout_builder']) as $delta) {
Chris@5 131 $section = $build['_layout_builder'][$delta];
Chris@5 132 /** @var \Drupal\Core\Layout\LayoutDefinition $layout */
Chris@5 133 $layout = $section['#layout'];
Chris@5 134 $regions = $layout->getRegionNames();
Chris@5 135
Chris@5 136 foreach ($regions as $region) {
Chris@5 137 if (isset($section[$region])) {
Chris@5 138 foreach ($section[$region] as $uuid => $component) {
Chris@5 139 if (isset($component['#plugin_id']) && $this->supportQuickEditOnComponent($component, $entity)) {
Chris@5 140 $plugin_ids_to_update[$component['#plugin_id']][$delta][$region][$uuid] = $uuid;
Chris@5 141 }
Chris@5 142 }
Chris@5 143 }
Chris@5 144 }
Chris@5 145 }
Chris@5 146
Chris@5 147 // @todo Remove when https://www.drupal.org/node/3041850 is resolved.
Chris@5 148 $plugin_ids_to_update = array_filter($plugin_ids_to_update, function ($info) {
Chris@5 149 // Delta, region, and UUID each count as one.
Chris@5 150 return count($info, COUNT_RECURSIVE) === 3;
Chris@5 151 });
Chris@5 152
Chris@5 153 $plugin_ids_to_update = NestedArray::mergeDeepArray($plugin_ids_to_update, TRUE);
Chris@5 154 foreach ($plugin_ids_to_update as $delta => $regions) {
Chris@5 155 foreach ($regions as $region => $uuids) {
Chris@5 156 foreach ($uuids as $uuid => $component) {
Chris@5 157 $build['_layout_builder'][$delta][$region][$uuid]['content']['#view_mode'] = static::getViewModeId($entity, $display, $delta, $uuid, $sections_hash);
Chris@5 158 }
Chris@5 159 }
Chris@5 160 }
Chris@5 161 // Alter the Quick Edit view mode ID of all fields outside of the Layout
Chris@5 162 // Builder sections to force Quick Edit to request to the field metadata.
Chris@5 163 // @todo Remove this logic in https://www.drupal.org/project/node/2966136.
Chris@5 164 foreach (Element::children($build) as $field_name) {
Chris@5 165 if ($field_name !== '_layout_builder') {
Chris@5 166 $field_build = &$build[$field_name];
Chris@5 167 if (isset($field_build['#view_mode'])) {
Chris@5 168 $field_build['#view_mode'] = "layout_builder-{$display->getMode()}-non_component-$sections_hash";
Chris@5 169 }
Chris@5 170 }
Chris@5 171 }
Chris@5 172 }
Chris@5 173
Chris@5 174 /**
Chris@5 175 * Generates a Quick Edit view mode ID.
Chris@5 176 *
Chris@5 177 * @param \Drupal\Core\Entity\EntityInterface $entity
Chris@5 178 * The entity.
Chris@5 179 * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
Chris@5 180 * The entity view display.
Chris@5 181 * @param int $delta
Chris@5 182 * The delta.
Chris@5 183 * @param string $component_uuid
Chris@5 184 * The component UUID.
Chris@5 185 * @param string $sections_hash
Chris@5 186 * The hash of the sections; must change whenever the sections change.
Chris@5 187 *
Chris@5 188 * @return string
Chris@5 189 * The Quick Edit view mode ID.
Chris@5 190 *
Chris@5 191 * @see \Drupal\layout_builder\QuickEditIntegration::deconstructViewModeId()
Chris@5 192 */
Chris@5 193 private static function getViewModeId(EntityInterface $entity, EntityViewDisplayInterface $display, $delta, $component_uuid, $sections_hash) {
Chris@5 194 return implode('-', [
Chris@5 195 'layout_builder',
Chris@5 196 $display->getMode(),
Chris@5 197 $delta,
Chris@5 198 // Replace the dashes in the component UUID because we need to
Chris@5 199 // use dashes to join the parts.
Chris@5 200 str_replace('-', '_', $component_uuid),
Chris@5 201 $entity->id(),
Chris@5 202 $sections_hash,
Chris@5 203 ]);
Chris@5 204 }
Chris@5 205
Chris@5 206 /**
Chris@5 207 * Deconstructs the Quick Edit view mode ID into its constituent parts.
Chris@5 208 *
Chris@5 209 * @param string $quick_edit_view_mode_id
Chris@5 210 * The Quick Edit view mode ID.
Chris@5 211 *
Chris@5 212 * @return array
Chris@5 213 * An array containing the entity view mode ID, the delta, the component
Chris@5 214 * UUID, and the entity ID.
Chris@5 215 *
Chris@5 216 * @see \Drupal\layout_builder\QuickEditIntegration::getViewModeId()
Chris@5 217 */
Chris@5 218 public static function deconstructViewModeId($quick_edit_view_mode_id) {
Chris@5 219 list(, $entity_view_mode_id, $delta, $component_uuid, $entity_id) = explode('-', $quick_edit_view_mode_id, 7);
Chris@5 220 return [
Chris@5 221 $entity_view_mode_id,
Chris@5 222 // @todo Explicitly cast delta to an integer, remove this in
Chris@5 223 // https://www.drupal.org/project/drupal/issues/2984509.
Chris@5 224 (int) $delta,
Chris@5 225 // Replace the underscores with dash to get back the component UUID.
Chris@5 226 str_replace('_', '-', $component_uuid),
Chris@5 227 $entity_id,
Chris@5 228 ];
Chris@5 229 }
Chris@5 230
Chris@5 231 /**
Chris@5 232 * Re-renders a field rendered by Layout Builder, edited with Quick Edit.
Chris@5 233 *
Chris@5 234 * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
Chris@5 235 * The entity.
Chris@5 236 * @param string $field_name
Chris@5 237 * The field name.
Chris@5 238 * @param string $quick_edit_view_mode_id
Chris@5 239 * The Quick Edit view mode ID.
Chris@5 240 * @param string $langcode
Chris@5 241 * The language code.
Chris@5 242 *
Chris@5 243 * @return array
Chris@5 244 * The re-rendered field.
Chris@5 245 */
Chris@5 246 public function quickEditRenderField(FieldableEntityInterface $entity, $field_name, $quick_edit_view_mode_id, $langcode) {
Chris@5 247 list($entity_view_mode, $delta, $component_uuid) = static::deconstructViewModeId($quick_edit_view_mode_id);
Chris@5 248
Chris@5 249 $entity_build = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId())->view($entity, $entity_view_mode, $langcode);
Chris@5 250 $this->buildEntityView($entity_build);
Chris@5 251
Chris@5 252 if (isset($entity_build['_layout_builder'][$delta])) {
Chris@5 253 foreach (Element::children($entity_build['_layout_builder'][$delta]) as $region) {
Chris@5 254 if (isset($entity_build['_layout_builder'][$delta][$region][$component_uuid])) {
Chris@5 255 return $entity_build['_layout_builder'][$delta][$region][$component_uuid]['content'];
Chris@5 256 }
Chris@5 257 }
Chris@5 258 }
Chris@5 259
Chris@5 260 $this->getLogger('layout_builder')->warning('The field "%field" failed to render.', ['%field' => $field_name]);
Chris@5 261 return [];
Chris@5 262 }
Chris@5 263
Chris@5 264 /**
Chris@5 265 * {@inheritdoc}
Chris@5 266 *
Chris@5 267 * @todo Replace this hardcoded processing when
Chris@5 268 * https://www.drupal.org/project/drupal/issues/3041635 is resolved.
Chris@5 269 *
Chris@5 270 * @see \Drupal\Tests\EntityViewTrait::buildEntityView()
Chris@5 271 */
Chris@5 272 private function buildEntityView(array &$elements) {
Chris@5 273 // If the default values for this element have not been loaded yet,
Chris@5 274 // populate them.
Chris@5 275 if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
Chris@5 276 $elements += \Drupal::service('element_info')->getInfo($elements['#type']);
Chris@5 277 }
Chris@5 278
Chris@5 279 // Make any final changes to the element before it is rendered. This means
Chris@5 280 // that the $element or the children can be altered or corrected before
Chris@5 281 // the element is rendered into the final text.
Chris@5 282 if (isset($elements['#pre_render'])) {
Chris@5 283 foreach ($elements['#pre_render'] as $callable) {
Chris@5 284 $elements = call_user_func($callable, $elements);
Chris@5 285 }
Chris@5 286 }
Chris@5 287
Chris@5 288 // And recurse.
Chris@5 289 $children = Element::children($elements, TRUE);
Chris@5 290 foreach ($children as $key) {
Chris@5 291 $this->buildEntityView($elements[$key]);
Chris@5 292 }
Chris@5 293 }
Chris@5 294
Chris@5 295 /**
Chris@5 296 * Determines whether a component has Quick Edit support.
Chris@5 297 *
Chris@5 298 * Only field_block components for display configurable fields should be
Chris@5 299 * supported.
Chris@5 300 *
Chris@5 301 * @param array $component
Chris@5 302 * The component render array.
Chris@5 303 * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
Chris@5 304 * The entity being displayed.
Chris@5 305 *
Chris@5 306 * @return bool
Chris@5 307 * Whether Quick Edit is supported on the component.
Chris@5 308 *
Chris@5 309 * @see \Drupal\layout_builder\Plugin\Block\FieldBlock
Chris@5 310 */
Chris@5 311 private function supportQuickEditOnComponent(array $component, FieldableEntityInterface $entity) {
Chris@5 312 if (isset($component['content']['#field_name'], $component['#base_plugin_id']) && $component['#base_plugin_id'] === 'field_block' && $entity->hasField($component['content']['#field_name'])) {
Chris@5 313 return $entity->getFieldDefinition($component['content']['#field_name'])->isDisplayConfigurable('view');
Chris@5 314 }
Chris@5 315 return FALSE;
Chris@5 316 }
Chris@5 317
Chris@5 318 }