1<?php
2
3namespace Drupal\field_layout;
4
5use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
6use Drupal\Core\Entity\EntityFieldManagerInterface;
7use Drupal\Core\Field\FieldDefinitionInterface;
8use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
9use Drupal\Core\Layout\LayoutPluginManagerInterface;
10use Symfony\Component\DependencyInjection\ContainerInterface;
11
12/**
13 * Builds a field layout.
14 */
15class FieldLayoutBuilder implements ContainerInjectionInterface {
16
17  /**
18   * The layout plugin manager.
19   *
20   * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
21   */
22  protected $layoutPluginManager;
23
24  /**
25   * The entity field manager.
26   *
27   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
28   */
29  protected $entityFieldManager;
30
31  /**
32   * Constructs a new FieldLayoutBuilder.
33   *
34   * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_plugin_manager
35   *   The layout plugin manager.
36   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
37   *   The entity field manager.
38   */
39  public function __construct(LayoutPluginManagerInterface $layout_plugin_manager, EntityFieldManagerInterface $entity_field_manager) {
40    $this->layoutPluginManager = $layout_plugin_manager;
41    $this->entityFieldManager = $entity_field_manager;
42  }
43
44  /**
45   * {@inheritdoc}
46   */
47  public static function create(ContainerInterface $container) {
48    return new static(
49      $container->get('plugin.manager.core.layout'),
50      $container->get('entity_field.manager')
51    );
52  }
53
54  /**
55   * Applies the layout to an entity build.
56   *
57   * @param array $build
58   *   A renderable array representing the entity content or form.
59   * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display
60   *   The entity display holding the display options configured for the entity
61   *   components.
62   */
63  public function buildView(array &$build, EntityDisplayWithLayoutInterface $display) {
64    $layout_definition = $this->layoutPluginManager->getDefinition($display->getLayoutId(), FALSE);
65    if ($layout_definition && $fields = $this->getFields($build, $display, 'view')) {
66      // Add the regions to the $build in the correct order.
67      $regions = array_fill_keys($layout_definition->getRegionNames(), []);
68
69      foreach ($fields as $name => $field) {
70        // If the region is controlled by the layout, move the field from the
71        // top-level of $build into a region-specific section. Custom regions
72        // could be set by other code at run-time; these should be ignored.
73        // @todo Ideally the array structure would remain unchanged, see
74        //   https://www.drupal.org/node/2846393.
75        if (isset($regions[$field['region']])) {
76          $regions[$field['region']][$name] = $build[$name];
77          unset($build[$name]);
78        }
79      }
80      // Ensure this will not conflict with any existing array elements by
81      // prefixing with an underscore.
82      $build['_field_layout'] = $display->getLayout()->build($regions);
83    }
84  }
85
86  /**
87   * Applies the layout to an entity form.
88   *
89   * @param array $build
90   *   A renderable array representing the entity content or form.
91   * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display
92   *   The entity display holding the display options configured for the entity
93   *   components.
94   */
95  public function buildForm(array &$build, EntityDisplayWithLayoutInterface $display) {
96    $layout_definition = $this->layoutPluginManager->getDefinition($display->getLayoutId(), FALSE);
97    if ($layout_definition && $fields = $this->getFields($build, $display, 'form')) {
98      $fill = [];
99      $fill['#process'][] = '\Drupal\Core\Render\Element\RenderElement::processGroup';
100      $fill['#pre_render'][] = '\Drupal\Core\Render\Element\RenderElement::preRenderGroup';
101      // Add the regions to the $build in the correct order.
102      $regions = array_fill_keys($layout_definition->getRegionNames(), $fill);
103
104      foreach ($fields as $name => $field) {
105        // As this is a form, #group can be used to relocate the fields. This
106        // avoids breaking hook_form_alter() implementations by not actually
107        // moving the field in the form structure. If a #group is already set,
108        // do not overwrite it.
109        if (isset($regions[$field['region']]) && !isset($build[$name]['#group'])) {
110          $build[$name]['#group'] = $field['region'];
111        }
112      }
113      // Ensure this will not conflict with any existing array elements by
114      // prefixing with an underscore.
115      $build['_field_layout'] = $display->getLayout()->build($regions);
116    }
117  }
118
119  /**
120   * Gets the fields that need to be processed.
121   *
122   * @param array $build
123   *   A renderable array representing the entity content or form.
124   * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display
125   *   The entity display holding the display options configured for the entity
126   *   components.
127   * @param string $display_context
128   *   The display context, either 'form' or 'view'.
129   *
130   * @return array
131   *   An array of configurable fields present in the build.
132   */
133  protected function getFields(array $build, EntityDisplayWithLayoutInterface $display, $display_context) {
134    $components = $display->getComponents();
135
136    // Ignore any extra fields from the list of field definitions. Field
137    // definitions can have a non-configurable display, but all extra fields are
138    // always displayed.
139    $field_definitions = array_diff_key(
140      $this->entityFieldManager->getFieldDefinitions($display->getTargetEntityTypeId(), $display->getTargetBundle()),
141      $this->entityFieldManager->getExtraFields($display->getTargetEntityTypeId(), $display->getTargetBundle())
142    );
143
144    $fields_to_exclude = array_filter($field_definitions, function (FieldDefinitionInterface $field_definition) use ($display_context) {
145      // Remove fields with a non-configurable display.
146      return !$field_definition->isDisplayConfigurable($display_context);
147    });
148    $components = array_diff_key($components, $fields_to_exclude);
149
150    // Only include fields present in the build.
151    $components = array_intersect_key($components, $build);
152
153    return $components;
154  }
155
156}
157