Chris@14
|
1 <?php
|
Chris@14
|
2
|
Chris@14
|
3 namespace Drupal\layout_builder\Entity;
|
Chris@14
|
4
|
Chris@14
|
5 use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
|
Chris@14
|
6 use Drupal\Component\Plugin\DependentPluginInterface;
|
Chris@14
|
7 use Drupal\Component\Plugin\PluginInspectionInterface;
|
Chris@14
|
8 use Drupal\Component\Utility\NestedArray;
|
Chris@14
|
9 use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay;
|
Chris@14
|
10 use Drupal\Core\Entity\EntityStorageInterface;
|
Chris@14
|
11 use Drupal\Core\Entity\FieldableEntityInterface;
|
Chris@14
|
12 use Drupal\Core\Plugin\Context\Context;
|
Chris@14
|
13 use Drupal\Core\Plugin\Context\ContextDefinition;
|
Chris@14
|
14 use Drupal\Core\Plugin\Definition\DependentPluginDefinitionInterface;
|
Chris@14
|
15 use Drupal\Core\StringTranslation\TranslatableMarkup;
|
Chris@14
|
16 use Drupal\field\Entity\FieldConfig;
|
Chris@14
|
17 use Drupal\field\Entity\FieldStorageConfig;
|
Chris@14
|
18 use Drupal\layout_builder\Section;
|
Chris@14
|
19 use Drupal\layout_builder\SectionComponent;
|
Chris@14
|
20 use Drupal\layout_builder\SectionStorage\SectionStorageTrait;
|
Chris@14
|
21
|
Chris@14
|
22 /**
|
Chris@14
|
23 * Provides an entity view display entity that has a layout.
|
Chris@14
|
24 *
|
Chris@14
|
25 * @internal
|
Chris@14
|
26 * Layout Builder is currently experimental and should only be leveraged by
|
Chris@14
|
27 * experimental modules and development releases of contributed modules.
|
Chris@14
|
28 * See https://www.drupal.org/core/experimental for more information.
|
Chris@14
|
29 */
|
Chris@14
|
30 class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements LayoutEntityDisplayInterface {
|
Chris@14
|
31
|
Chris@14
|
32 use SectionStorageTrait;
|
Chris@14
|
33
|
Chris@14
|
34 /**
|
Chris@14
|
35 * {@inheritdoc}
|
Chris@14
|
36 */
|
Chris@14
|
37 public function isOverridable() {
|
Chris@14
|
38 return $this->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
|
Chris@14
|
39 }
|
Chris@14
|
40
|
Chris@14
|
41 /**
|
Chris@14
|
42 * {@inheritdoc}
|
Chris@14
|
43 */
|
Chris@14
|
44 public function setOverridable($overridable = TRUE) {
|
Chris@14
|
45 $this->setThirdPartySetting('layout_builder', 'allow_custom', $overridable);
|
Chris@14
|
46 return $this;
|
Chris@14
|
47 }
|
Chris@14
|
48
|
Chris@14
|
49 /**
|
Chris@14
|
50 * {@inheritdoc}
|
Chris@14
|
51 */
|
Chris@14
|
52 public function getSections() {
|
Chris@14
|
53 return $this->getThirdPartySetting('layout_builder', 'sections', []);
|
Chris@14
|
54 }
|
Chris@14
|
55
|
Chris@14
|
56 /**
|
Chris@14
|
57 * {@inheritdoc}
|
Chris@14
|
58 */
|
Chris@14
|
59 protected function setSections(array $sections) {
|
Chris@14
|
60 $this->setThirdPartySetting('layout_builder', 'sections', array_values($sections));
|
Chris@14
|
61 return $this;
|
Chris@14
|
62 }
|
Chris@14
|
63
|
Chris@14
|
64 /**
|
Chris@14
|
65 * {@inheritdoc}
|
Chris@14
|
66 */
|
Chris@14
|
67 public function preSave(EntityStorageInterface $storage) {
|
Chris@14
|
68 parent::preSave($storage);
|
Chris@14
|
69
|
Chris@14
|
70 $original_value = isset($this->original) ? $this->original->isOverridable() : FALSE;
|
Chris@14
|
71 $new_value = $this->isOverridable();
|
Chris@14
|
72 if ($original_value !== $new_value) {
|
Chris@14
|
73 $entity_type_id = $this->getTargetEntityTypeId();
|
Chris@14
|
74 $bundle = $this->getTargetBundle();
|
Chris@14
|
75
|
Chris@14
|
76 if ($new_value) {
|
Chris@14
|
77 $this->addSectionField($entity_type_id, $bundle, 'layout_builder__layout');
|
Chris@14
|
78 }
|
Chris@14
|
79 elseif ($field = FieldConfig::loadByName($entity_type_id, $bundle, 'layout_builder__layout')) {
|
Chris@14
|
80 $field->delete();
|
Chris@14
|
81 }
|
Chris@14
|
82 }
|
Chris@14
|
83 }
|
Chris@14
|
84
|
Chris@14
|
85 /**
|
Chris@14
|
86 * Adds a layout section field to a given bundle.
|
Chris@14
|
87 *
|
Chris@14
|
88 * @param string $entity_type_id
|
Chris@14
|
89 * The entity type ID.
|
Chris@14
|
90 * @param string $bundle
|
Chris@14
|
91 * The bundle.
|
Chris@14
|
92 * @param string $field_name
|
Chris@14
|
93 * The name for the layout section field.
|
Chris@14
|
94 */
|
Chris@14
|
95 protected function addSectionField($entity_type_id, $bundle, $field_name) {
|
Chris@14
|
96 $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name);
|
Chris@14
|
97 if (!$field) {
|
Chris@14
|
98 $field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name);
|
Chris@14
|
99 if (!$field_storage) {
|
Chris@14
|
100 $field_storage = FieldStorageConfig::create([
|
Chris@14
|
101 'entity_type' => $entity_type_id,
|
Chris@14
|
102 'field_name' => $field_name,
|
Chris@14
|
103 'type' => 'layout_section',
|
Chris@14
|
104 'locked' => TRUE,
|
Chris@14
|
105 ]);
|
Chris@14
|
106 $field_storage->save();
|
Chris@14
|
107 }
|
Chris@14
|
108
|
Chris@14
|
109 $field = FieldConfig::create([
|
Chris@14
|
110 'field_storage' => $field_storage,
|
Chris@14
|
111 'bundle' => $bundle,
|
Chris@14
|
112 'label' => t('Layout'),
|
Chris@14
|
113 ]);
|
Chris@14
|
114 $field->save();
|
Chris@14
|
115 }
|
Chris@14
|
116 }
|
Chris@14
|
117
|
Chris@14
|
118 /**
|
Chris@14
|
119 * {@inheritdoc}
|
Chris@14
|
120 */
|
Chris@14
|
121 protected function getDefaultRegion() {
|
Chris@14
|
122 if ($this->hasSection(0)) {
|
Chris@14
|
123 return $this->getSection(0)->getDefaultRegion();
|
Chris@14
|
124 }
|
Chris@14
|
125
|
Chris@14
|
126 return parent::getDefaultRegion();
|
Chris@14
|
127 }
|
Chris@14
|
128
|
Chris@14
|
129 /**
|
Chris@14
|
130 * Wraps the context repository service.
|
Chris@14
|
131 *
|
Chris@14
|
132 * @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface
|
Chris@14
|
133 * The context repository service.
|
Chris@14
|
134 */
|
Chris@14
|
135 protected function contextRepository() {
|
Chris@14
|
136 return \Drupal::service('context.repository');
|
Chris@14
|
137 }
|
Chris@14
|
138
|
Chris@14
|
139 /**
|
Chris@14
|
140 * {@inheritdoc}
|
Chris@14
|
141 */
|
Chris@14
|
142 public function buildMultiple(array $entities) {
|
Chris@14
|
143 $build_list = parent::buildMultiple($entities);
|
Chris@14
|
144
|
Chris@14
|
145 foreach ($entities as $id => $entity) {
|
Chris@14
|
146 $sections = $this->getRuntimeSections($entity);
|
Chris@14
|
147 if ($sections) {
|
Chris@14
|
148 foreach ($build_list[$id] as $name => $build_part) {
|
Chris@14
|
149 $field_definition = $this->getFieldDefinition($name);
|
Chris@14
|
150 if ($field_definition && $field_definition->isDisplayConfigurable($this->displayContext)) {
|
Chris@14
|
151 unset($build_list[$id][$name]);
|
Chris@14
|
152 }
|
Chris@14
|
153 }
|
Chris@14
|
154
|
Chris@14
|
155 // Bypass ::getContexts() in order to use the runtime entity, not a
|
Chris@14
|
156 // sample entity.
|
Chris@14
|
157 $contexts = $this->contextRepository()->getAvailableContexts();
|
Chris@14
|
158 // @todo Use EntityContextDefinition after resolving
|
Chris@14
|
159 // https://www.drupal.org/node/2932462.
|
Chris@14
|
160 $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity);
|
Chris@14
|
161 foreach ($sections as $delta => $section) {
|
Chris@14
|
162 $build_list[$id]['_layout_builder'][$delta] = $section->toRenderArray($contexts);
|
Chris@14
|
163 }
|
Chris@14
|
164 }
|
Chris@14
|
165 }
|
Chris@14
|
166
|
Chris@14
|
167 return $build_list;
|
Chris@14
|
168 }
|
Chris@14
|
169
|
Chris@14
|
170 /**
|
Chris@14
|
171 * Gets the runtime sections for a given entity.
|
Chris@14
|
172 *
|
Chris@14
|
173 * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
|
Chris@14
|
174 * The entity.
|
Chris@14
|
175 *
|
Chris@14
|
176 * @return \Drupal\layout_builder\Section[]
|
Chris@14
|
177 * The sections.
|
Chris@14
|
178 */
|
Chris@14
|
179 protected function getRuntimeSections(FieldableEntityInterface $entity) {
|
Chris@14
|
180 if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) {
|
Chris@14
|
181 return $entity->get('layout_builder__layout')->getSections();
|
Chris@14
|
182 }
|
Chris@14
|
183
|
Chris@14
|
184 return $this->getSections();
|
Chris@14
|
185 }
|
Chris@14
|
186
|
Chris@14
|
187 /**
|
Chris@14
|
188 * {@inheritdoc}
|
Chris@14
|
189 *
|
Chris@14
|
190 * @todo Move this upstream in https://www.drupal.org/node/2939931.
|
Chris@14
|
191 */
|
Chris@14
|
192 public function label() {
|
Chris@14
|
193 $bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getTargetEntityTypeId());
|
Chris@14
|
194 $bundle_label = $bundle_info[$this->getTargetBundle()]['label'];
|
Chris@14
|
195 $target_entity_type = $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId());
|
Chris@14
|
196 return new TranslatableMarkup('@bundle @label', ['@bundle' => $bundle_label, '@label' => $target_entity_type->getPluralLabel()]);
|
Chris@14
|
197 }
|
Chris@14
|
198
|
Chris@14
|
199 /**
|
Chris@14
|
200 * {@inheritdoc}
|
Chris@14
|
201 */
|
Chris@14
|
202 public function calculateDependencies() {
|
Chris@14
|
203 parent::calculateDependencies();
|
Chris@14
|
204
|
Chris@14
|
205 foreach ($this->getSections() as $delta => $section) {
|
Chris@14
|
206 $this->calculatePluginDependencies($section->getLayout());
|
Chris@14
|
207 foreach ($section->getComponents() as $uuid => $component) {
|
Chris@14
|
208 $this->calculatePluginDependencies($component->getPlugin());
|
Chris@14
|
209 }
|
Chris@14
|
210 }
|
Chris@14
|
211
|
Chris@14
|
212 return $this;
|
Chris@14
|
213 }
|
Chris@14
|
214
|
Chris@14
|
215 /**
|
Chris@14
|
216 * {@inheritdoc}
|
Chris@14
|
217 */
|
Chris@14
|
218 public function onDependencyRemoval(array $dependencies) {
|
Chris@14
|
219 $changed = parent::onDependencyRemoval($dependencies);
|
Chris@14
|
220
|
Chris@14
|
221 // Loop through all sections and determine if the removed dependencies are
|
Chris@14
|
222 // used by their layout plugins.
|
Chris@14
|
223 foreach ($this->getSections() as $delta => $section) {
|
Chris@14
|
224 $layout_dependencies = $this->getPluginDependencies($section->getLayout());
|
Chris@14
|
225 $layout_removed_dependencies = $this->getPluginRemovedDependencies($layout_dependencies, $dependencies);
|
Chris@14
|
226 if ($layout_removed_dependencies) {
|
Chris@14
|
227 // @todo Allow the plugins to react to their dependency removal in
|
Chris@14
|
228 // https://www.drupal.org/project/drupal/issues/2579743.
|
Chris@14
|
229 $this->removeSection($delta);
|
Chris@14
|
230 $changed = TRUE;
|
Chris@14
|
231 }
|
Chris@14
|
232 // If the section is not removed, loop through all components.
|
Chris@14
|
233 else {
|
Chris@14
|
234 foreach ($section->getComponents() as $uuid => $component) {
|
Chris@14
|
235 $plugin_dependencies = $this->getPluginDependencies($component->getPlugin());
|
Chris@14
|
236 $component_removed_dependencies = $this->getPluginRemovedDependencies($plugin_dependencies, $dependencies);
|
Chris@14
|
237 if ($component_removed_dependencies) {
|
Chris@14
|
238 // @todo Allow the plugins to react to their dependency removal in
|
Chris@14
|
239 // https://www.drupal.org/project/drupal/issues/2579743.
|
Chris@14
|
240 $section->removeComponent($uuid);
|
Chris@14
|
241 $changed = TRUE;
|
Chris@14
|
242 }
|
Chris@14
|
243 }
|
Chris@14
|
244 }
|
Chris@14
|
245 }
|
Chris@14
|
246 return $changed;
|
Chris@14
|
247 }
|
Chris@14
|
248
|
Chris@14
|
249 /**
|
Chris@14
|
250 * Calculates and returns dependencies of a specific plugin instance.
|
Chris@14
|
251 *
|
Chris@14
|
252 * @param \Drupal\Component\Plugin\PluginInspectionInterface $instance
|
Chris@14
|
253 * The plugin instance.
|
Chris@14
|
254 *
|
Chris@14
|
255 * @return array
|
Chris@14
|
256 * An array of dependencies keyed by the type of dependency.
|
Chris@14
|
257 *
|
Chris@14
|
258 * @todo Replace this in https://www.drupal.org/project/drupal/issues/2939925.
|
Chris@14
|
259 */
|
Chris@14
|
260 protected function getPluginDependencies(PluginInspectionInterface $instance) {
|
Chris@14
|
261 $dependencies = [];
|
Chris@14
|
262 $definition = $instance->getPluginDefinition();
|
Chris@14
|
263 if ($definition instanceof PluginDefinitionInterface) {
|
Chris@14
|
264 $dependencies['module'][] = $definition->getProvider();
|
Chris@14
|
265 if ($definition instanceof DependentPluginDefinitionInterface && $config_dependencies = $definition->getConfigDependencies()) {
|
Chris@14
|
266 $dependencies = NestedArray::mergeDeep($dependencies, $config_dependencies);
|
Chris@14
|
267 }
|
Chris@14
|
268 }
|
Chris@14
|
269 elseif (is_array($definition)) {
|
Chris@14
|
270 $dependencies['module'][] = $definition['provider'];
|
Chris@14
|
271 // Plugins can declare additional dependencies in their definition.
|
Chris@14
|
272 if (isset($definition['config_dependencies'])) {
|
Chris@14
|
273 $dependencies = NestedArray::mergeDeep($dependencies, $definition['config_dependencies']);
|
Chris@14
|
274 }
|
Chris@14
|
275 }
|
Chris@14
|
276
|
Chris@14
|
277 // If a plugin is dependent, calculate its dependencies.
|
Chris@14
|
278 if ($instance instanceof DependentPluginInterface && $plugin_dependencies = $instance->calculateDependencies()) {
|
Chris@14
|
279 $dependencies = NestedArray::mergeDeep($dependencies, $plugin_dependencies);
|
Chris@14
|
280 }
|
Chris@14
|
281 return $dependencies;
|
Chris@14
|
282 }
|
Chris@14
|
283
|
Chris@14
|
284 /**
|
Chris@14
|
285 * {@inheritdoc}
|
Chris@14
|
286 */
|
Chris@14
|
287 public function setComponent($name, array $options = []) {
|
Chris@14
|
288 parent::setComponent($name, $options);
|
Chris@14
|
289
|
Chris@14
|
290 // @todo Remove workaround for EntityViewBuilder::getSingleFieldDisplay() in
|
Chris@14
|
291 // https://www.drupal.org/project/drupal/issues/2936464.
|
Chris@14
|
292 if ($this->getMode() === static::CUSTOM_MODE) {
|
Chris@14
|
293 return $this;
|
Chris@14
|
294 }
|
Chris@14
|
295
|
Chris@14
|
296 // Retrieve the updated options after the parent:: call.
|
Chris@14
|
297 $options = $this->content[$name];
|
Chris@14
|
298 // Provide backwards compatibility by converting to a section component.
|
Chris@14
|
299 $field_definition = $this->getFieldDefinition($name);
|
Chris@14
|
300 if ($field_definition && $field_definition->isDisplayConfigurable('view') && isset($options['type'])) {
|
Chris@14
|
301 $configuration = [];
|
Chris@14
|
302 $configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
|
Chris@14
|
303 $configuration['label_display'] = FALSE;
|
Chris@14
|
304 $keys = array_flip(['type', 'label', 'settings', 'third_party_settings']);
|
Chris@14
|
305 $configuration['formatter'] = array_intersect_key($options, $keys);
|
Chris@14
|
306 $configuration['context_mapping']['entity'] = 'layout_builder.entity';
|
Chris@14
|
307
|
Chris@14
|
308 $section = $this->getDefaultSection();
|
Chris@14
|
309 $region = isset($options['region']) ? $options['region'] : $section->getDefaultRegion();
|
Chris@14
|
310 $new_component = (new SectionComponent(\Drupal::service('uuid')->generate(), $region, $configuration));
|
Chris@14
|
311 $section->appendComponent($new_component);
|
Chris@14
|
312 }
|
Chris@14
|
313 return $this;
|
Chris@14
|
314 }
|
Chris@14
|
315
|
Chris@14
|
316 /**
|
Chris@14
|
317 * Gets a default section.
|
Chris@14
|
318 *
|
Chris@14
|
319 * @return \Drupal\layout_builder\Section
|
Chris@14
|
320 * The default section.
|
Chris@14
|
321 */
|
Chris@14
|
322 protected function getDefaultSection() {
|
Chris@14
|
323 // If no section exists, append a new one.
|
Chris@14
|
324 if (!$this->hasSection(0)) {
|
Chris@14
|
325 $this->appendSection(new Section('layout_onecol'));
|
Chris@14
|
326 }
|
Chris@14
|
327
|
Chris@14
|
328 // Return the first section.
|
Chris@14
|
329 return $this->getSection(0);
|
Chris@14
|
330 }
|
Chris@14
|
331
|
Chris@14
|
332 }
|