Skip to content

Commit 46c3418

Browse files
author
Stephen Lucero
committed
Issue #3507554 by slucero, johnle, minsharm: Add Event Dispatching for Pattern Render Failures
1 parent d3ff9d0 commit 46c3418

10 files changed

Lines changed: 640 additions & 103 deletions

File tree

‎patternkit.services.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ services:
142142
class: Drupal\patternkit\Routing\PatternkitTranslateBlockFormRouteSubscriber
143143
tags:
144144
- { name: event_subscriber }
145+
patternkit.subscriber.render_error:
146+
class: Drupal\patternkit\EventSubscriber\RenderFailureErrorDisplaySubscriber
147+
tags:
148+
- { name: event_subscriber }
145149
patternkit.environment:
146150
class: Drupal\patternkit\PatternkitEnvironment
147151
arguments: ['%patternkit.config%']

‎src/Element/Pattern.php‎

Lines changed: 75 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@
33
namespace Drupal\patternkit\Element;
44

55
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
6+
use Drupal\Core\Render\Attribute\RenderElement;
67
use Drupal\Core\Render\BubbleableMetadata;
78
use Drupal\Core\Render\Element;
89
use Drupal\Core\Render\Element\RenderElementBase;
910
use Drupal\patternkit\Entity\PatternInterface;
10-
use Drupal\patternkit\Exception\PatternAssetException;
11+
use Drupal\patternkit\Event\PatternRenderFailureEvent;
1112
use Drupal\patternkit\Exception\UnknownPatternException;
1213
use Drupal\patternkit\PatternFieldProcessorPluginManager;
14+
use Drupal\patternkit\PatternkitEvents;
1315
use Drupal\patternkit\PatternLibraryPluginInterface;
1416
use Drupal\patternkit\PatternLibraryPluginManager;
1517
use Drupal\patternkit\PatternRepository;
1618
use Symfony\Component\DependencyInjection\ContainerInterface;
19+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1720

1821
/**
1922
* Provides a render element to display a pattern.
@@ -38,31 +41,15 @@
3841
* ];
3942
* @endcode
4043
*
41-
* @RenderElement("pattern")
44+
* @see \Drupal\patternkit\Element\PatternError
45+
* @see \Drupal\patternkit\PatternkitEvents::RENDER_FAILURE
46+
* @see \Drupal\patternkit\EventSubscriber\RenderFailureErrorDisplaySubscriber
4247
*/
48+
#[RenderElement(
49+
id: "pattern",
50+
)]
4351
class Pattern extends RenderElementBase implements ContainerFactoryPluginInterface {
4452

45-
/**
46-
* The pattern field processor plugin manager.
47-
*
48-
* @var \Drupal\patternkit\PatternFieldProcessorPluginManager
49-
*/
50-
protected PatternFieldProcessorPluginManager $fieldProcessorPluginManager;
51-
52-
/**
53-
* The pattern library plugin manager.
54-
*
55-
* @var \Drupal\patternkit\PatternLibraryPluginManager
56-
*/
57-
protected PatternLibraryPluginManager $libraryPluginManager;
58-
59-
/**
60-
* The pattern repository service for loading patterns.
61-
*
62-
* @var \Drupal\patternkit\PatternRepository
63-
*/
64-
protected PatternRepository $patternRepository;
65-
6653
/**
6754
* {@inheritdoc}
6855
*/
@@ -93,20 +80,19 @@ public function getInfo(): array {
9380
* The pattern library parser plugin manager service.
9481
* @param \Drupal\patternkit\PatternRepository $patternRepository
9582
* The pattern repository service.
83+
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
84+
* The event dispatcher service.
9685
*/
9786
public function __construct(
9887
array $configuration,
9988
$plugin_id,
10089
$plugin_definition,
101-
PatternFieldProcessorPluginManager $fieldProcessorPluginManager,
102-
PatternLibraryPluginManager $libraryPluginManager,
103-
PatternRepository $patternRepository,
90+
protected PatternFieldProcessorPluginManager $fieldProcessorPluginManager,
91+
protected PatternLibraryPluginManager $libraryPluginManager,
92+
protected PatternRepository $patternRepository,
93+
protected EventDispatcherInterface $eventDispatcher,
10494
) {
10595
parent::__construct($configuration, $plugin_id, $plugin_definition);
106-
107-
$this->fieldProcessorPluginManager = $fieldProcessorPluginManager;
108-
$this->libraryPluginManager = $libraryPluginManager;
109-
$this->patternRepository = $patternRepository;
11096
}
11197

11298
/**
@@ -120,6 +106,7 @@ public static function create(ContainerInterface $container, array $configuratio
120106
$container->get('plugin.manager.pattern_field_processor'),
121107
$container->get('plugin.manager.library.pattern'),
122108
$container->get('patternkit.pattern.repository'),
109+
$container->get('event_dispatcher'),
123110
);
124111
}
125112

@@ -133,26 +120,27 @@ public static function create(ContainerInterface $container, array $configuratio
133120
* The modified element.
134121
*/
135122
public function loadPattern(array $element): array {
123+
// If the pattern is specified as a string, assume it is the pattern name
124+
// and attempt to load it.
136125
if (isset($element['#pattern']) && is_string($element['#pattern'])) {
137126
$pattern_id = $element['#pattern'];
138127

139128
try {
140129
$element['#pattern'] = $this->patternRepository->getPattern($pattern_id);
141130
}
142-
catch (UnknownPatternException $exception) {
143-
// Replace the pattern output with an error element for more
144-
// sophisticated output handling.
145-
// @see \Drupal\patternkit\Element\PatternError
146-
$elements = [];
147-
$elements['error'] = [
148-
'#type' => 'pattern_error',
149-
'#pattern' => $element['#pattern'],
150-
'#config' => $element['#config'] ?? [],
151-
'#context' => $element['#context'] ?? [],
152-
'#exception' => $exception,
153-
];
131+
catch (\Throwable $exception) {
132+
// Dispatch an event for the render failure to allow subscribers to
133+
// react and handle it appropriately.
134+
$event = $this->dispatchRenderFailureEvent($element, $exception);
154135

155-
return $elements;
136+
// If the event was handled, update the elements to be returned.
137+
if ($event->isHandled()) {
138+
return $event->getElement();
139+
}
140+
else {
141+
// Propagate the exception if an event dispatcher didn't handle it.
142+
throw $exception;
143+
}
156144
}
157145
}
158146

@@ -167,47 +155,46 @@ public function loadPattern(array $element): array {
167155
*
168156
* @return array
169157
* The modified element.
170-
*
171-
* @throws \Drupal\Component\Plugin\Exception\PluginException
172158
*/
173159
public function preRenderPatternElement(array $element): array {
174-
/** @var \Drupal\patternkit\Entity\PatternInterface|null $pattern */
175-
$pattern = $element['#pattern'];
176-
177-
// Fail early if a pattern was unable to be loaded.
178-
if ($pattern === NULL) {
179-
return [
180-
'#markup' => $this->t('Pattern unavailable.'),
181-
];
182-
}
183-
184160
// Capture base bubbleable metadata.
185161
$metadata = BubbleableMetadata::createFromRenderArray($element);
186162

187-
// Ensure the metadata from the pattern is present.
188-
$metadata->addCacheableDependency($pattern);
189-
190-
$pattern->config = $element['#config'];
191-
$pattern->context = $element['#context'];
163+
$pattern = $element['#pattern'] ?? NULL;
192164

193165
try {
166+
// Fail early if a pattern was unable to be loaded.
167+
if (!($pattern instanceof PatternInterface)) {
168+
throw new UnknownPatternException('No pattern entity available for rendering.');
169+
}
170+
171+
// Ensure the metadata from the pattern is present.
172+
$metadata->addCacheableDependency($pattern);
173+
174+
// Capture instance data for the pattern and apply preprocessing
175+
// throughout schema values.
176+
$pattern->config = $element['#config'];
177+
$pattern->context = $element['#context'];
194178
$this->fieldProcessorPluginManager->processSchemaValues($pattern, $pattern->config, $pattern->context, $metadata);
195179

180+
// Use the pattern's library plugin to produce rendered output.
196181
$library_plugin = $this->getPatternLibraryPlugin($pattern);
197182
$elements = $library_plugin->render([$pattern]);
198183
}
199-
catch (PatternAssetException $exception) {
200-
// Replace the pattern output with an error element for more sophisticated
201-
// output handling.
202-
// @see \Drupal\patternkit\Element\PatternError
203-
$elements = [];
204-
$elements['error'] = [
205-
'#type' => 'pattern_error',
206-
'#pattern' => $element['#pattern'],
207-
'#config' => $element['#config'],
208-
'#context' => $element['#context'],
209-
'#exception' => $exception,
210-
];
184+
catch (\Throwable $exception) {
185+
// Dispatch an event for the render failure to allow subscribers to react
186+
// and handle it appropriately.
187+
$event = $this->dispatchRenderFailureEvent($element, $exception);
188+
189+
// If the event was handled, update the elements to be returned.
190+
if ($event->isHandled()) {
191+
$elements = $event->getElement();
192+
}
193+
else {
194+
$elements = [
195+
'#markup' => $this->t('This content is unavailable.'),
196+
];
197+
}
211198
}
212199

213200
// Ensure all metadata persists to the result.
@@ -251,4 +238,20 @@ protected function getPatternLibraryPlugin(PatternInterface $pattern): PatternLi
251238
return $plugin;
252239
}
253240

241+
/**
242+
* Create a new render failure event instance for dispatching.
243+
*
244+
* @param array $element
245+
* The render element resulting in a failure.
246+
* @param \Throwable $exception
247+
* The exception encountered leading to the render failure.
248+
*
249+
* @return \Drupal\patternkit\Event\PatternRenderFailureEvent
250+
* The configured render failure event for dispatching.
251+
*/
252+
protected function dispatchRenderFailureEvent(array $element, \Throwable $exception): PatternRenderFailureEvent {
253+
$event = new PatternRenderFailureEvent($element, $exception);
254+
return $this->eventDispatcher->dispatch($event, PatternkitEvents::RENDER_FAILURE);
255+
}
256+
254257
}

‎src/Element/PatternError.php‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Drupal\patternkit\Element;
44

5+
use Drupal\Core\Render\Attribute\RenderElement;
56
use Drupal\Core\Render\Element\RenderElementBase;
67
use Drupal\patternkit\Entity\Pattern;
78

@@ -19,6 +20,7 @@
1920
* - '#exception': The exception object causing the render failure.
2021
*
2122
* Usage Example:
23+
*
2224
* @code
2325
* $build['failed_pattern'] = [
2426
* '#type' => 'pattern_error',
@@ -34,8 +36,13 @@
3436
* ];
3537
* @endcode
3638
*
37-
* @RenderElement("pattern_error")
39+
* @see \Drupal\patternkit\Element\Pattern
40+
* @see \Drupal\patternkit\PatternkitEvents::RENDER_FAILURE
41+
* @see \Drupal\patternkit\EventSubscriber\RenderFailureErrorDisplaySubscriber
3842
*/
43+
#[RenderElement(
44+
id: 'pattern_error',
45+
)]
3946
class PatternError extends RenderElementBase {
4047

4148
/**
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
3+
namespace Drupal\patternkit\Event;
4+
5+
use Drupal\Component\EventDispatcher\Event;
6+
7+
/**
8+
* An event triggered when a pattern encounters exceptions during rendering.
9+
*
10+
* @event
11+
*
12+
* @see \Drupal\patternkit\PatternkitEvents::RENDER_FAILURE
13+
*/
14+
class PatternRenderFailureEvent extends Event {
15+
16+
/**
17+
* A flag to track whether the failure event has been handled.
18+
*
19+
* @var bool
20+
*/
21+
protected bool $isHandled = FALSE;
22+
23+
/**
24+
* The render element array that encountered a failure.
25+
*
26+
* @var array
27+
*/
28+
protected array $element;
29+
30+
/**
31+
* Create a new PatternRenderFailureEvent instance.
32+
*
33+
* @param array $originalElement
34+
* The original render element that failed to render.
35+
* @param \Throwable $exception
36+
* The exception that lead to the render failure.
37+
*/
38+
public function __construct(
39+
protected readonly array $originalElement,
40+
protected readonly \Throwable $exception,
41+
) {
42+
$this->element = $originalElement;
43+
}
44+
45+
/**
46+
* Checks or sets whether the failure event has been handled.
47+
*
48+
* @param bool|null $isHandled
49+
* (optional) The new handled state.
50+
*
51+
* @return bool
52+
* TRUE if the event has been handled, FALSE otherwise.
53+
*/
54+
public function isHandled(?bool $isHandled = NULL): bool {
55+
if (!is_null($isHandled)) {
56+
$this->isHandled = $isHandled;
57+
}
58+
59+
return $this->isHandled;
60+
}
61+
62+
/**
63+
* Gets the original render element array.
64+
*
65+
* @return array
66+
* The original render element array.
67+
*/
68+
public function getOriginalElement(): array {
69+
return $this->originalElement;
70+
}
71+
72+
/**
73+
* Gets the render element array.
74+
*
75+
* This render element may have been altered by other event subscribers since
76+
* the original exception was raised. For the original element structure use
77+
* `::getOriginalElement()`.
78+
*
79+
* @return array
80+
* The render element array.
81+
*
82+
* @see self::getOriginalElement()
83+
*/
84+
public function getElement(): array {
85+
return $this->element;
86+
}
87+
88+
/**
89+
* Gets the exception that caused the render failure.
90+
*
91+
* @return \Throwable
92+
* The exception instance.
93+
*/
94+
public function getException(): \Throwable {
95+
return $this->exception;
96+
}
97+
98+
/**
99+
* Sets the render element.
100+
*
101+
* @param array $element
102+
* The new render element.
103+
*
104+
* @return self
105+
* The current instance for method chaining.
106+
*/
107+
public function setElement(array $element): self {
108+
$this->element = $element;
109+
110+
return $this;
111+
}
112+
113+
}

0 commit comments

Comments
 (0)