Skip to content

Commit a04593e

Browse files
Add event subscriber to include situation updates for full banner width alerts (#16883)
* add event subscriber to include situation updates for full banner width alerts * add a computed field example for obtaining situation updates for banners * try commenting out computed field defintion to see if it affects test failures I cannot view * uncomment code after checking impact on CI checks * add new controller for banner alerts * adding cache tags to response * add WIP test but pause work * add tests for each banner type * cleanup from review --------- Co-authored-by: Tanner Heffner <[email protected]>
1 parent 9ec08e8 commit a04593e

File tree

5 files changed

+509
-378
lines changed

5 files changed

+509
-378
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
<?php
2+
3+
namespace Drupal\va_gov_api\Controller;
4+
5+
use Drupal\Core\Cache\CacheableJsonResponse;
6+
use Drupal\Core\Controller\ControllerBase;
7+
use Drupal\Core\Entity\EntityTypeManagerInterface;
8+
use Drupal\Core\Path\PathMatcherInterface;
9+
use Drupal\Core\Path\PathValidatorInterface;
10+
use Symfony\Component\DependencyInjection\ContainerInterface;
11+
use Symfony\Component\HttpFoundation\JsonResponse;
12+
use Symfony\Component\HttpFoundation\Request;
13+
use Symfony\Component\Serializer\SerializerInterface;
14+
15+
/**
16+
* Returns responses for the Banner Alerts API.
17+
*
18+
* Currently, there are three banner types included in the response:
19+
* - Banner
20+
* - Promo Banner
21+
* - Full Width Banner Alert.
22+
*/
23+
class BannerAlertsController extends ControllerBase {
24+
25+
/**
26+
* The serializer service.
27+
*
28+
* @var \Symfony\Component\Serializer\SerializerInterface
29+
*/
30+
protected $serializer;
31+
32+
/**
33+
* Constructor.
34+
*
35+
* @param \Symfony\Component\Serializer\SerializerInterface $serializer
36+
* The serializer service.
37+
* @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
38+
* The path matcher service.
39+
* @param \Drupal\Core\Path\PathValidatorInterface $path_validator
40+
* The path validator service.
41+
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
42+
* The entity type manager service.
43+
*/
44+
public function __construct(
45+
SerializerInterface $serializer,
46+
PathMatcherInterface $path_matcher,
47+
PathValidatorInterface $path_validator,
48+
EntityTypeManagerInterface $entity_type_manager) {
49+
$this->serializer = $serializer;
50+
$this->pathMatcher = $path_matcher;
51+
$this->pathValidator = $path_validator;
52+
$this->entityTypeManager = $entity_type_manager;
53+
}
54+
55+
/**
56+
* {@inheritdoc}
57+
*/
58+
public static function create(ContainerInterface $container) {
59+
return new static(
60+
$container->get('serializer'),
61+
$container->get('path.matcher'),
62+
$container->get('path.validator'),
63+
$container->get('entity_type.manager')
64+
);
65+
}
66+
67+
/**
68+
* Get banner alerts by path.
69+
*
70+
* @param \Symfony\Component\HttpFoundation\Request $request
71+
* The request object.
72+
*
73+
* @return \Symfony\Component\HttpFoundation\JsonResponse
74+
* The banner alerts.
75+
*/
76+
public function bannerAlertsByPath(Request $request) {
77+
$path = $request->get('path');
78+
if (!$path) {
79+
return new JsonResponse(['error' => 'No path provided.']);
80+
}
81+
82+
[$banners, $banner_cache_tags] = $this->collectBannerData($path);
83+
[$promo_banners, $promo_cache_tags] = $this->collectPromoBannerData($path);
84+
[$full_width_banner_alerts, $full_width_banner_alert_cache_tags] = $this->collectFullWidthBannerAlertData($path);
85+
86+
$response = new CacheableJsonResponse([
87+
'data' => array_merge($banners, $promo_banners, $full_width_banner_alerts),
88+
]);
89+
90+
// Add the 'path' query parameter to the cache contexts.
91+
$response->getCacheableMetadata()->addCacheContexts(['url.query_args:path']);
92+
93+
// Add the cache tags from the banner nodes and set TTL.
94+
$cache_tags = array_merge($banner_cache_tags, $promo_cache_tags, $full_width_banner_alert_cache_tags);
95+
$response->getCacheableMetadata()->addCacheTags($cache_tags);
96+
$one_day = 60 * 60 * 24;
97+
$response->getCacheableMetadata()->setCacheMaxAge($one_day);
98+
99+
return $response;
100+
}
101+
102+
/**
103+
* Collect `banner` entities to be returned in the response.
104+
*
105+
* Given a path, retrieves any `banner` that should show there, constructs a
106+
* response for it, and adds it to cacheable dependencies.
107+
*
108+
* @param string $path
109+
* The path to the item to find banners for.
110+
*/
111+
protected function collectBannerData(string $path): array {
112+
$node_storage = $this->entityTypeManager->getStorage('node');
113+
114+
// Get all published banner nodes.
115+
$banner_nids = $node_storage->getQuery()
116+
->condition('type', 'banner')
117+
->condition('status', TRUE)
118+
->accessCheck(FALSE)
119+
->execute();
120+
/** @var \Drupal\node\NodeInterface[] $banners */
121+
$banners = $node_storage->loadMultiple(array_values($banner_nids) ?? []);
122+
123+
// Filter the banner list to just the ones that should be displayed for the
124+
// provided item path.
125+
$banners = array_filter($banners, function ($item) use ($path) {
126+
// PathMatcher expects a newline delimited string for multiple paths.
127+
$patterns = '';
128+
foreach ($item->field_target_paths->getValue() as $target_path) {
129+
$patterns .= $target_path['value'] . "\n";
130+
}
131+
132+
return $this->pathMatcher->matchPath($path, $patterns);
133+
});
134+
135+
// Add the banners to the response.
136+
$banner_data = [];
137+
$cache_tags = [];
138+
foreach ($banners as $entity) {
139+
$banner_data[] = $this->serializer->normalize($entity);
140+
$cache_tags = array_merge($cache_tags, $entity->getCacheTags());
141+
}
142+
143+
return [$this->flattenData($banner_data), $cache_tags];
144+
}
145+
146+
/**
147+
* Collect `promo_banner` entities to be returned in the response.
148+
*
149+
* Given a path, retrieves any `promo_banner` that should show there,
150+
* constructs a Response for it, and adds it to cacheable dependencies.
151+
*
152+
* @param string $path
153+
* The path to the item to find promo_banners for.
154+
*/
155+
protected function collectPromoBannerData(string $path): array {
156+
$node_storage = $this->entityTypeManager->getStorage('node');
157+
158+
// Get all published promo_banner nodes.
159+
$promo_banner_nids = $node_storage->getQuery()
160+
->condition('type', 'promo_banner')
161+
->condition('status', TRUE)
162+
->accessCheck(FALSE)
163+
->execute();
164+
/** @var \Drupal\node\NodeInterface[] $promo_banners */
165+
$promo_banners = $node_storage->loadMultiple(array_values($promo_banner_nids) ?? []);
166+
167+
// Filter the promo_banner list to just the ones that should be displayed
168+
// for the provided item path.
169+
$promo_banners = array_filter($promo_banners, function ($item) use ($path) {
170+
// PathMatcher expects a newline delimited string for multiple paths.
171+
$patterns = '';
172+
foreach ($item->field_target_paths->getValue() as $target_path) {
173+
$patterns .= $target_path['value'] . "\n";
174+
}
175+
176+
return $this->pathMatcher->matchPath($path, $patterns);
177+
});
178+
179+
// Add the promo_banners to the response.
180+
$promo_banner_data = [];
181+
$cache_tags = [];
182+
foreach ($promo_banners as $entity) {
183+
$promo_banner_data[] = $this->serializer->normalize($entity);
184+
$cache_tags = array_merge($cache_tags, $entity->getCacheTags());
185+
}
186+
return [$this->flattenData($promo_banner_data), $cache_tags];
187+
}
188+
189+
/**
190+
* Collect `full_width_banner_alert` entities to be returned in the response.
191+
*
192+
* Given a path, retrieves any `full_width_banner_alert` that should show
193+
* there, constructs a response for it, and adds it to
194+
* cacheable dependencies.
195+
*
196+
* @param string $path
197+
* The path to the item to find full_width_banner_alerts for.
198+
*/
199+
protected function collectFullWidthBannerAlertData(string $path): array {
200+
// Find the first fragment of the path; this will correspond to a facility,
201+
// if this is a facility page of some kind.
202+
$region_fragment = '__not_a_real_url';
203+
$path_pieces = explode("/", $path);
204+
if (count($path_pieces) > 1) {
205+
$region_fragment = "/" . $path_pieces[1];
206+
}
207+
208+
// Resolve the region fragment to a URL object.
209+
$url = $this->pathValidator->getUrlIfValidWithoutAccessCheck($region_fragment);
210+
if ($url === FALSE || !$url->isRouted() || !isset($url->getRouteParameters()['node'])) {
211+
// If the alias is invalid, it's not a routed URL, or there is not a node
212+
// in the route params, there's not much else that can be done here.
213+
return [[], []];
214+
}
215+
216+
// Load the system that we found.
217+
$node_storage = $this->entityTypeManager->getStorage('node');
218+
$system_nid = $url->getRouteParameters()['node'];
219+
/** @var \Drupal\node\NodeInterface $system_node */
220+
$system_node = $node_storage->load($system_nid);
221+
222+
// If it's not a published VAMC system node, bail early.
223+
if (is_null($system_node) || $system_node->getType() != 'health_care_region_page' || $system_node->isPublished() === FALSE) {
224+
return [[], []];
225+
}
226+
227+
// Find all operating status nodes which have this system as their office.
228+
$operating_status_nids = $node_storage->getQuery()
229+
->condition('type', 'vamc_operating_status_and_alerts')
230+
->condition('status', TRUE)
231+
->condition('field_office', $system_node->id())
232+
->accessCheck(FALSE)
233+
->execute();
234+
235+
// If there are no operating status nids, bail.
236+
if (count($operating_status_nids) === 0) {
237+
return [[], []];
238+
}
239+
240+
// Find any facility banners connected to the operating status nodes.
241+
$facility_banner_nids = $node_storage->getQuery()
242+
->condition('type', 'full_width_banner_alert')
243+
->condition('status', TRUE)
244+
->condition('field_banner_alert_vamcs', array_values($operating_status_nids), 'IN')
245+
->accessCheck(FALSE)
246+
->execute();
247+
248+
/** @var \Drupal\node\NodeInterface[] $facility_banners */
249+
$facility_banners = $node_storage->loadMultiple($facility_banner_nids);
250+
251+
// Add the banners to the response.
252+
$full_width_banner_alert_data = [];
253+
$cache_tags = [];
254+
foreach ($facility_banners as $entity) {
255+
$normalized_data = $this->serializer->normalize($entity);
256+
257+
// Override field_situation_updates with referenced paragraph data.
258+
$situation_updates = $entity->get('field_situation_updates')->referencedEntities();
259+
$situation_update_data = [];
260+
foreach ($situation_updates as $situation_update) {
261+
$situation_update_data[] = $this->serializer->normalize($situation_update);
262+
}
263+
$normalized_data['field_situation_updates'] = $this->flattenData($situation_update_data);
264+
265+
$full_width_banner_alert_data[] = $normalized_data;
266+
$cache_tags = array_merge($cache_tags, $entity->getCacheTags());
267+
}
268+
269+
return [$this->flattenData($full_width_banner_alert_data), $cache_tags];
270+
}
271+
272+
/**
273+
* Format the data into a flatter structure.
274+
*
275+
* @param array $data
276+
* The data to flatten.
277+
*
278+
* @return array
279+
* The flattened data.
280+
*/
281+
private function flattenData(array $data = []): array {
282+
return array_map(function ($item) {
283+
$result = [];
284+
foreach ($item as $key => $value) {
285+
// Check if value is an array with exactly one element.
286+
if (is_array($value) && count($value) === 1) {
287+
// Get the first element of the array.
288+
$firstElement = reset($value);
289+
290+
// Check if the first element itself is an associative array
291+
// with exactly one key.
292+
if (is_array($firstElement)
293+
&& count($firstElement) === 1
294+
&& array_key_exists('value', $firstElement)) {
295+
// Assign the 'value' directly.
296+
$result[$key] = $firstElement['value'];
297+
}
298+
else {
299+
// Keep the first element as is,
300+
// since it's an associative array with multiple keys.
301+
$result[$key] = $firstElement;
302+
}
303+
}
304+
else {
305+
// Copy the value as is.
306+
$result[$key] = $value;
307+
}
308+
}
309+
return $result;
310+
},
311+
$data);
312+
}
313+
314+
}

0 commit comments

Comments
 (0)