Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
ApiControllerBase
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
5
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
n/a
0 / 0
n/a
0 / 0
100.00% covered (success)
100.00%
1 / 1
1
 dispatchDisplayBuilderEvent
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createEventWithEnabledIsland
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 saveSseData
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Controller;
6
7use Drupal\Component\Datetime\TimeInterface;
8use Drupal\Core\Controller\ControllerBase;
9use Drupal\Core\Render\RendererInterface;
10use Drupal\Core\TempStore\SharedTempStoreFactory;
11use Drupal\display_builder\Entity\ProfileInterface;
12use Drupal\display_builder\Event\DisplayBuilderEvent;
13use Drupal\display_builder\Event\DisplayBuilderEvents;
14use Drupal\display_builder\InstanceInterface;
15use Symfony\Component\DependencyInjection\Attribute\Autowire;
16use Symfony\Component\HttpFoundation\Session\SessionInterface;
17use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
18
19/**
20 * Returns responses for Display builder routes.
21 */
22abstract class ApiControllerBase extends ControllerBase {
23
24  public const string SSE_COLLECTION = 'display_builder_sse';
25
26  /**
27   * The list of DB events which triggers SSE refresh.
28   *
29   * ON_ACTIVE is intentionally excluded: it is a client-side presence signal
30   * that must not trigger a full SSE broadcast to avoid feedback loops.
31   */
32  public const array SSE_EVENTS = [
33    DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
34    DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
35    DisplayBuilderEvents::ON_DELETE,
36    DisplayBuilderEvents::ON_HISTORY_CHANGE,
37    DisplayBuilderEvents::ON_MOVE,
38    DisplayBuilderEvents::ON_PRESET_SAVE,
39    DisplayBuilderEvents::ON_PUBLISH,
40    DisplayBuilderEvents::ON_UPDATE,
41  ];
42
43  /**
44   * The Display Builder instance triggering the action.
45   */
46  protected InstanceInterface $builder;
47
48  /**
49   * Plugin ID of the island triggering the HTMX event.
50   *
51   * If not NULL, the island will be skipped from the event dispatch. Useful to
52   * avoid swapping the content of an island which is already in the expected
53   * state. For examples, if we move an instance in Builder, Layers or Tree
54   * panels, if we change the settings in InstanceForm.
55   *
56   * @see \Drupal\display_builder\Event\DisplayBuilderEventsSubscriber::dispatchToIslands()
57   * @see \Drupal\display_builder\HtmxEvents
58   */
59  protected ?string $islandId = NULL;
60
61  /**
62   * The lazy loaded display builder.
63   */
64  protected ?ProfileInterface $displayBuilder = NULL;
65
66  public function __construct(
67    protected EventDispatcherInterface $eventDispatcher,
68    protected RendererInterface $renderer,
69    protected TimeInterface $time,
70    #[Autowire(service: 'tempstore.shared')]
71    protected SharedTempStoreFactory $sharedTempStoreFactory,
72    protected SessionInterface $session,
73  ) {}
74
75  /**
76   * Dispatches a display builder event.
77   *
78   * @param string $event_id
79   *   The event ID.
80   * @param array|null $data
81   *   The data.
82   * @param string|null $node_id
83   *   Optional instance ID.
84   * @param string|null $parent_id
85   *   Optional parent ID.
86   *
87   * @return array
88   *   A renderable array.
89   */
90  protected function dispatchDisplayBuilderEvent(
91    string $event_id,
92    ?array $data = NULL,
93    ?string $node_id = NULL,
94    ?string $parent_id = NULL,
95  ): array {
96    $event = $this->createEventWithEnabledIsland($event_id, $data, $node_id, $parent_id);
97    $this->saveSseData($event_id);
98
99    return $event->getResult();
100  }
101
102  /**
103   * Creates a display builder event with enabled islands only.
104   *
105   * Use a cache to avoid loading all the builder configuration.
106   *
107   * @param string $event_id
108   *   The event ID.
109   * @param array|null $data
110   *   The data.
111   * @param string|null $node_id
112   *   Optional Instance entity ID.
113   * @param string|null $parent_id
114   *   Optional parent ID.
115   *
116   * @return \Drupal\display_builder\Event\DisplayBuilderEvent
117   *   The event.
118   */
119  protected function createEventWithEnabledIsland(string $event_id, ?array $data, ?string $node_id, ?string $parent_id): DisplayBuilderEvent {
120    $event = new DisplayBuilderEvent($this->builder, $data, $node_id, $parent_id, $this->islandId);
121    $this->eventDispatcher->dispatch($event, $event_id);
122
123    return $event;
124  }
125
126  /**
127   * Save data for SSE.
128   *
129   * @param string $event_id
130   *   The event ID.
131   */
132  protected function saveSseData(string $event_id): void {
133    if (!\in_array($event_id, $this::SSE_EVENTS, TRUE)) {
134      return;
135    }
136
137    $state = [
138      'sessionId' => $this->session->getId(),
139      'timestamp' => $this->time->getRequestTime(),
140      // instanceId here is the ID of a display_builder_instance entity.
141      'instanceId' => (string) $this->builder->id(),
142    ];
143    $collection = $this->sharedTempStoreFactory->get($this::SSE_COLLECTION);
144    $collection->set(\sprintf('%s_latest', (string) $this->builder->id()), $state);
145  }
146
147}