Pull merge.
[yaffs-website] / web / core / modules / layout_builder / src / Entity / LayoutBuilderEntityViewDisplay.php
1 <?php
2
3 namespace Drupal\layout_builder\Entity;
4
5 use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay;
6 use Drupal\Core\Entity\EntityStorageInterface;
7 use Drupal\Core\Entity\FieldableEntityInterface;
8 use Drupal\Core\Plugin\Context\EntityContext;
9 use Drupal\Core\StringTranslation\TranslatableMarkup;
10 use Drupal\field\Entity\FieldConfig;
11 use Drupal\field\Entity\FieldStorageConfig;
12 use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
13 use Drupal\layout_builder\Section;
14 use Drupal\layout_builder\SectionComponent;
15 use Drupal\layout_builder\SectionStorage\SectionStorageTrait;
16
17 /**
18  * Provides an entity view display entity that has a layout.
19  *
20  * @internal
21  *   Layout Builder is currently experimental and should only be leveraged by
22  *   experimental modules and development releases of contributed modules.
23  *   See https://www.drupal.org/core/experimental for more information.
24  */
25 class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements LayoutEntityDisplayInterface {
26
27   use SectionStorageTrait;
28
29   /**
30    * The entity field manager.
31    *
32    * @var \Drupal\Core\Entity\EntityFieldManagerInterface
33    */
34   protected $entityFieldManager;
35
36   /**
37    * {@inheritdoc}
38    */
39   public function __construct(array $values, $entity_type) {
40     // Set $entityFieldManager before calling the parent constructor because the
41     // constructor will call init() which then calls setComponent() which needs
42     // $entityFieldManager.
43     $this->entityFieldManager = \Drupal::service('entity_field.manager');
44     parent::__construct($values, $entity_type);
45   }
46
47   /**
48    * {@inheritdoc}
49    */
50   public function isOverridable() {
51     return $this->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
52   }
53
54   /**
55    * {@inheritdoc}
56    */
57   public function setOverridable($overridable = TRUE) {
58     $this->setThirdPartySetting('layout_builder', 'allow_custom', $overridable);
59     return $this;
60   }
61
62   /**
63    * {@inheritdoc}
64    */
65   public function isLayoutBuilderEnabled() {
66     return (bool) $this->getThirdPartySetting('layout_builder', 'enabled');
67   }
68
69   /**
70    * {@inheritdoc}
71    */
72   public function enableLayoutBuilder() {
73     $this->setThirdPartySetting('layout_builder', 'enabled', TRUE);
74     return $this;
75   }
76
77   /**
78    * {@inheritdoc}
79    */
80   public function disableLayoutBuilder() {
81     $this->setOverridable(FALSE);
82     $this->setThirdPartySetting('layout_builder', 'enabled', FALSE);
83     return $this;
84   }
85
86   /**
87    * {@inheritdoc}
88    */
89   public function getSections() {
90     return $this->getThirdPartySetting('layout_builder', 'sections', []);
91   }
92
93   /**
94    * {@inheritdoc}
95    */
96   protected function setSections(array $sections) {
97     $this->setThirdPartySetting('layout_builder', 'sections', array_values($sections));
98     return $this;
99   }
100
101   /**
102    * {@inheritdoc}
103    */
104   public function preSave(EntityStorageInterface $storage) {
105     parent::preSave($storage);
106
107     $original_value = isset($this->original) ? $this->original->isOverridable() : FALSE;
108     $new_value = $this->isOverridable();
109     if ($original_value !== $new_value) {
110       $entity_type_id = $this->getTargetEntityTypeId();
111       $bundle = $this->getTargetBundle();
112
113       if ($new_value) {
114         $this->addSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
115       }
116       else {
117         $this->removeSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
118       }
119     }
120
121     $already_enabled = isset($this->original) ? $this->original->isLayoutBuilderEnabled() : FALSE;
122     $set_enabled = $this->isLayoutBuilderEnabled();
123     if ($already_enabled !== $set_enabled) {
124       if ($set_enabled) {
125         // Loop through all existing field-based components and add them as
126         // section-based components.
127         $components = $this->getComponents();
128         // Sort the components by weight.
129         uasort($components, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
130         foreach ($components as $name => $component) {
131           $this->setComponent($name, $component);
132         }
133       }
134       else {
135         // When being disabled, remove all existing section data.
136         while (count($this) > 0) {
137           $this->removeSection(0);
138         }
139       }
140     }
141   }
142
143   /**
144    * Removes a layout section field if it is no longer needed.
145    *
146    * Because the field is shared across all view modes, the field will only be
147    * removed if no other view modes are using it.
148    *
149    * @param string $entity_type_id
150    *   The entity type ID.
151    * @param string $bundle
152    *   The bundle.
153    * @param string $field_name
154    *   The name for the layout section field.
155    */
156   protected function removeSectionField($entity_type_id, $bundle, $field_name) {
157     $query = $this->entityTypeManager()->getStorage($this->getEntityTypeId())->getQuery()
158       ->condition('targetEntityType', $this->getTargetEntityTypeId())
159       ->condition('bundle', $this->getTargetBundle())
160       ->condition('mode', $this->getMode(), '<>')
161       ->condition('third_party_settings.layout_builder.allow_custom', TRUE);
162     $enabled = (bool) $query->count()->execute();
163     if (!$enabled && $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name)) {
164       $field->delete();
165     }
166   }
167
168   /**
169    * Adds a layout section field to a given bundle.
170    *
171    * @param string $entity_type_id
172    *   The entity type ID.
173    * @param string $bundle
174    *   The bundle.
175    * @param string $field_name
176    *   The name for the layout section field.
177    */
178   protected function addSectionField($entity_type_id, $bundle, $field_name) {
179     $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name);
180     if (!$field) {
181       $field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name);
182       if (!$field_storage) {
183         $field_storage = FieldStorageConfig::create([
184           'entity_type' => $entity_type_id,
185           'field_name' => $field_name,
186           'type' => 'layout_section',
187           'locked' => TRUE,
188         ]);
189         $field_storage->save();
190       }
191
192       $field = FieldConfig::create([
193         'field_storage' => $field_storage,
194         'bundle' => $bundle,
195         'label' => t('Layout'),
196       ]);
197       $field->save();
198     }
199   }
200
201   /**
202    * {@inheritdoc}
203    */
204   public function createCopy($mode) {
205     // Disable Layout Builder and remove any sections copied from the original.
206     return parent::createCopy($mode)
207       ->setSections([])
208       ->disableLayoutBuilder();
209   }
210
211   /**
212    * {@inheritdoc}
213    */
214   protected function getDefaultRegion() {
215     if ($this->hasSection(0)) {
216       return $this->getSection(0)->getDefaultRegion();
217     }
218
219     return parent::getDefaultRegion();
220   }
221
222   /**
223    * Wraps the context repository service.
224    *
225    * @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface
226    *   The context repository service.
227    */
228   protected function contextRepository() {
229     return \Drupal::service('context.repository');
230   }
231
232   /**
233    * {@inheritdoc}
234    */
235   public function buildMultiple(array $entities) {
236     $build_list = parent::buildMultiple($entities);
237     if (!$this->isLayoutBuilderEnabled()) {
238       return $build_list;
239     }
240
241     /** @var \Drupal\Core\Entity\EntityInterface $entity */
242     foreach ($entities as $id => $entity) {
243       $sections = $this->getRuntimeSections($entity);
244       if ($sections) {
245         foreach ($build_list[$id] as $name => $build_part) {
246           $field_definition = $this->getFieldDefinition($name);
247           if ($field_definition && $field_definition->isDisplayConfigurable($this->displayContext)) {
248             unset($build_list[$id][$name]);
249           }
250         }
251
252         // Bypass ::getContexts() in order to use the runtime entity, not a
253         // sample entity.
254         $contexts = $this->contextRepository()->getAvailableContexts();
255         $label = new TranslatableMarkup('@entity being viewed', [
256           '@entity' => $entity->getEntityType()->getSingularLabel(),
257         ]);
258         $contexts['layout_builder.entity'] = EntityContext::fromEntity($entity, $label);
259         foreach ($sections as $delta => $section) {
260           $build_list[$id]['_layout_builder'][$delta] = $section->toRenderArray($contexts);
261         }
262       }
263     }
264
265     return $build_list;
266   }
267
268   /**
269    * Gets the runtime sections for a given entity.
270    *
271    * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
272    *   The entity.
273    *
274    * @return \Drupal\layout_builder\Section[]
275    *   The sections.
276    */
277   protected function getRuntimeSections(FieldableEntityInterface $entity) {
278     if ($this->isOverridable() && !$entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()) {
279       return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
280     }
281
282     return $this->getSections();
283   }
284
285   /**
286    * {@inheritdoc}
287    *
288    * @todo Move this upstream in https://www.drupal.org/node/2939931.
289    */
290   public function label() {
291     $bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getTargetEntityTypeId());
292     $bundle_label = $bundle_info[$this->getTargetBundle()]['label'];
293     $target_entity_type = $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId());
294     return new TranslatableMarkup('@bundle @label', ['@bundle' => $bundle_label, '@label' => $target_entity_type->getPluralLabel()]);
295   }
296
297   /**
298    * {@inheritdoc}
299    */
300   public function calculateDependencies() {
301     parent::calculateDependencies();
302
303     foreach ($this->getSections() as $delta => $section) {
304       $this->calculatePluginDependencies($section->getLayout());
305       foreach ($section->getComponents() as $uuid => $component) {
306         $this->calculatePluginDependencies($component->getPlugin());
307       }
308     }
309
310     return $this;
311   }
312
313   /**
314    * {@inheritdoc}
315    */
316   public function onDependencyRemoval(array $dependencies) {
317     $changed = parent::onDependencyRemoval($dependencies);
318
319     // Loop through all sections and determine if the removed dependencies are
320     // used by their layout plugins.
321     foreach ($this->getSections() as $delta => $section) {
322       $layout_dependencies = $this->getPluginDependencies($section->getLayout());
323       $layout_removed_dependencies = $this->getPluginRemovedDependencies($layout_dependencies, $dependencies);
324       if ($layout_removed_dependencies) {
325         // @todo Allow the plugins to react to their dependency removal in
326         //   https://www.drupal.org/project/drupal/issues/2579743.
327         $this->removeSection($delta);
328         $changed = TRUE;
329       }
330       // If the section is not removed, loop through all components.
331       else {
332         foreach ($section->getComponents() as $uuid => $component) {
333           $plugin_dependencies = $this->getPluginDependencies($component->getPlugin());
334           $component_removed_dependencies = $this->getPluginRemovedDependencies($plugin_dependencies, $dependencies);
335           if ($component_removed_dependencies) {
336             // @todo Allow the plugins to react to their dependency removal in
337             //   https://www.drupal.org/project/drupal/issues/2579743.
338             $section->removeComponent($uuid);
339             $changed = TRUE;
340           }
341         }
342       }
343     }
344     return $changed;
345   }
346
347   /**
348    * {@inheritdoc}
349    */
350   public function setComponent($name, array $options = []) {
351     parent::setComponent($name, $options);
352
353     // Only continue if Layout Builder is enabled.
354     if (!$this->isLayoutBuilderEnabled()) {
355       return $this;
356     }
357
358     // Retrieve the updated options after the parent:: call.
359     $options = $this->content[$name];
360     // Provide backwards compatibility by converting to a section component.
361     $field_definition = $this->getFieldDefinition($name);
362     $extra_fields = $this->entityFieldManager->getExtraFields($this->getTargetEntityTypeId(), $this->getTargetBundle());
363     $is_view_configurable_non_extra_field = $field_definition && $field_definition->isDisplayConfigurable('view') && isset($options['type']);
364     if ($is_view_configurable_non_extra_field || isset($extra_fields['display'][$name])) {
365       $configuration = [
366         'label_display' => '0',
367         'context_mapping' => ['entity' => 'layout_builder.entity'],
368       ];
369       if ($is_view_configurable_non_extra_field) {
370         $configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
371         $keys = array_flip(['type', 'label', 'settings', 'third_party_settings']);
372         $configuration['formatter'] = array_intersect_key($options, $keys);
373       }
374       else {
375         $configuration['id'] = 'extra_field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
376       }
377
378       $section = $this->getDefaultSection();
379       $region = isset($options['region']) ? $options['region'] : $section->getDefaultRegion();
380       $new_component = (new SectionComponent(\Drupal::service('uuid')->generate(), $region, $configuration));
381       $section->appendComponent($new_component);
382     }
383     return $this;
384   }
385
386   /**
387    * Gets a default section.
388    *
389    * @return \Drupal\layout_builder\Section
390    *   The default section.
391    */
392   protected function getDefaultSection() {
393     // If no section exists, append a new one.
394     if (!$this->hasSection(0)) {
395       $this->appendSection(new Section('layout_onecol'));
396     }
397
398     // Return the first section.
399     return $this->getSection(0);
400   }
401
402 }