Skip to content

Commit d3ff9d0

Browse files
author
Stephen Lucero
committed
Issue #3503179 by slucero, johnle: Add PatternUpdateEvent Subscriber Base Class and Validation Tools
1 parent 303854d commit d3ff9d0

22 files changed

Lines changed: 1835 additions & 62 deletions

‎phpstan.neon‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,6 @@ parameters:
5858
- *Test.php
5959

6060
# Ignore dependency injection warnings for intentional lazy-loader methods.
61-
- message: '#^\\Drupal calls should be avoided in classes, use dependency injection instead$#'
62-
count: 2
63-
path: src/Event/PatternUpdateEvent.php
6461
- message: '#^\\Drupal calls should be avoided in classes, use dependency injection instead$#'
6562
count: 1
6663
path: src/LayoutHelper.php

‎src/Event/PatternUpdateEvent.php‎

Lines changed: 101 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,29 @@
44

55
use Drupal\Component\EventDispatcher\Event;
66
use Drupal\patternkit\Entity\PatternInterface;
7-
use Drupal\patternkit\Exception\SchemaValidationException;
7+
use Drupal\patternkit\PatternValidationTrait;
88
use Drupal\patternkit\Plugin\Block\PatternkitBlock;
9-
use Swaggest\JsonSchema\InvalidValue;
109

1110
/**
1211
* An event triggered when a pattern in use is being updated.
1312
*
1413
* @event
1514
*
1615
* @see \Drupal\patternkit\PatternkitEvents::PATTERN_UPDATE
16+
* @see \Drupal\patternkit\EventSubscriber\PatternUpdateEventSubscriberBase
1717
*/
1818
class PatternUpdateEvent extends Event {
1919

20+
use PatternValidationTrait {
21+
validateContent as traitValidateContent;
22+
}
23+
2024
/**
2125
* Content for the pattern instance at the time of updating.
2226
*
2327
* @var array
2428
*/
25-
protected array $originalContent;
29+
protected readonly array $originalContent;
2630

2731
/**
2832
* Content for the pattern instance that may have been altered.
@@ -38,6 +42,13 @@ class PatternUpdateEvent extends Event {
3842
*/
3943
protected bool $isAltered = FALSE;
4044

45+
/**
46+
* A flag to track whether the contained pattern content is valid.
47+
*
48+
* @var bool
49+
*/
50+
protected bool $isValid;
51+
4152
/**
4253
* Create a new PatternUpdateEvent instance.
4354
*
@@ -91,24 +102,61 @@ public function isAltered(): bool {
91102
return $this->isAltered;
92103
}
93104

105+
/**
106+
* Determine if content for a component is valid for the pattern schema.
107+
*
108+
* @return bool
109+
* True if the content is valid, false otherwise.
110+
*/
111+
public function isValid(): bool {
112+
if (!isset($this->isValid)) {
113+
$this->isValid = $this->validateContent($this->getContent());
114+
}
115+
116+
return $this->isValid;
117+
}
118+
94119
/**
95120
* Assign new content for the component.
96121
*
97122
* @param array $content
98123
* Content values to be assigned to the pattern component.
124+
* @param bool $allowInvalid
125+
* (Optional) A flag to allow invalid content to be set and validation to be
126+
* ignored. Defaults to FALSE. The primary use case for this is when
127+
* supporting sequential content updates that may produce invalid content
128+
* along the way, but the final result after all updates have been executed
129+
* is expected to be valid.
99130
*
100131
* @throws \Drupal\patternkit\Exception\SchemaValidationException
101132
* Throws an exception when the provided content does not validate
102-
* against the updated pattern schema.
133+
* against the updated pattern schema. This is only thrown if $allowInvalid
134+
* remains FALSE.
135+
*
136+
* @see \Drupal\patternkit\Event\PatternUpdateEvent::getExceptions()
137+
*
138+
* @return $this
103139
*/
104-
public function setContent(array $content): void {
105-
if ($this->validateContent($content)) {
140+
public function setContent(array $content, bool $allowInvalid = FALSE): self {
141+
$isValid = $this->validateContent($content);
142+
143+
// Only throw the exceptions if invalid content is not allowed.
144+
if (!$isValid && !$allowInvalid) {
145+
$encountered_exceptions = $this->getExceptions();
146+
assert(count($encountered_exceptions) > 0);
147+
148+
// Throw the exception before content assignment if invalid content is not
149+
// allowed.
150+
throw $encountered_exceptions[0];
151+
}
152+
153+
if ($isValid || $allowInvalid) {
106154
$this->content = $content;
107155
$this->isAltered = TRUE;
156+
$this->isValid = $isValid;
108157
}
109-
else {
110-
throw new SchemaValidationException('Assigned content does not validate successfully.');
111-
}
158+
159+
return $this;
112160
}
113161

114162
/**
@@ -121,6 +169,46 @@ public function getPattern(): PatternInterface {
121169
return $this->newPattern;
122170
}
123171

172+
/**
173+
* Get the label for the updated pattern entity from schema data.
174+
*
175+
* @return string
176+
* The label for the updated pattern entity.
177+
*/
178+
public function getPatternName(): string {
179+
return $this->newPattern->getName();
180+
}
181+
182+
/**
183+
* Get the asset ID for the updated pattern entity.
184+
*
185+
* @return string
186+
* The pattern asset ID for the updated pattern entity.
187+
*/
188+
public function getPatternAssetId(): string {
189+
return $this->newPattern->getAssetId();
190+
}
191+
192+
/**
193+
* Get the label for the pre-update pattern entity from schema data.
194+
*
195+
* @return string
196+
* The label for the pre-update pattern entity.
197+
*/
198+
public function getOriginalPatternName(): string {
199+
return $this->oldPattern->getName();
200+
}
201+
202+
/**
203+
* Get the asset ID for the pre-update pattern entity.
204+
*
205+
* @return string
206+
* The pattern asset ID for the pre-update pattern entity.
207+
*/
208+
public function getOriginalPatternAssetId(): string {
209+
return $this->oldPattern->getAssetId();
210+
}
211+
124212
/**
125213
* Get the original pattern entity.
126214
*
@@ -149,28 +237,11 @@ public function getPlugin(): PatternkitBlock {
149237
*
150238
* @return bool
151239
* True if the content validates successfully. False otherwise.
152-
*
153-
* @todo Expose this somewhere as a standard validation solution.
154240
*/
155-
protected function validateContent(array $content): bool {
156-
$schema_string = $this->newPattern->getSchema();
157-
158-
/** @var \Drupal\patternkit\Schema\SchemaFactory $schema_factory */
159-
$schema_factory = \Drupal::service('patternkit.schema.schema_factory');
160-
$schema = $schema_factory->createInstance($schema_string);
161-
162-
/** @var \Drupal\patternkit\Schema\ContextBuilder $context_builder */
163-
$context_builder = \Drupal::service('patternkit.schema.context_builder');
164-
$context = $context_builder->getDefaultContext();
165-
166-
try {
167-
$schema->in($content, $context);
168-
}
169-
catch (InvalidValue $exception) {
170-
return FALSE;
171-
}
172-
173-
return TRUE;
241+
public function validateContent(array $content): bool {
242+
// This is overridden for convenience to automatically pass in the updated
243+
// pattern for reference.
244+
return $this->traitValidateContent($this->newPattern, $content);
174245
}
175246

176247
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
namespace Drupal\patternkit\EventSubscriber;
4+
5+
use Drupal\patternkit\Event\PatternUpdateEvent;
6+
use Drupal\patternkit\PatternkitEvents;
7+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
8+
9+
/**
10+
* A base event subscriber class to extend when subscribing to pattern updates.
11+
*
12+
* @see \Drupal\patternkit\PatternkitEvents::PATTERN_UPDATE
13+
* @see \Drupal\patternkit\Event\PatternUpdateEvent
14+
*/
15+
abstract class PatternUpdateEventSubscriberBase implements EventSubscriberInterface {
16+
17+
/**
18+
* The event subscriber priority for responding to the pattern update event.
19+
*
20+
* This may be overridden by inheriting classes to customize the order
21+
* subscribers are executed during an update event.
22+
*/
23+
const UPDATE_PRIORITY = 200;
24+
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
public static function getSubscribedEvents(): array {
29+
$events[PatternkitEvents::PATTERN_UPDATE] = [
30+
'onPatternUpdate',
31+
static::UPDATE_PRIORITY,
32+
];
33+
34+
return $events;
35+
}
36+
37+
/**
38+
* React to the pattern update process and log alter text values.
39+
*
40+
* Unless further customization is needed, inheriting classes should override
41+
* the related `isApplicable()` and `doUpdate()` methods instead of this one.
42+
*
43+
* @param \Drupal\patternkit\Event\PatternUpdateEvent $event
44+
* The pattern update event containing all update information.
45+
*
46+
* @see static::isApplicable()
47+
* @see static::doUpdate()
48+
*/
49+
public function onPatternUpdate(PatternUpdateEvent $event): void {
50+
if ($this->isApplicable($event)) {
51+
$this->doUpdate($event);
52+
}
53+
}
54+
55+
/**
56+
* Test whether an update is applicable for the current event.
57+
*
58+
* Inheriting classes should override this method to apply their own
59+
* conditional logic to determine if their subscriber should be applied to
60+
* the specific pattern update in progress.
61+
*
62+
* @param \Drupal\patternkit\Event\PatternUpdateEvent $event
63+
* The pattern update event in progress.
64+
*
65+
* @return bool
66+
* Returns true if this update subscriber is applicable for the current
67+
* event. Returns false otherwise.
68+
*
69+
* @see static::onPatternUpdate()
70+
*/
71+
abstract public function isApplicable(PatternUpdateEvent $event): bool;
72+
73+
/**
74+
* Implement update behavior for the active pattern update event.
75+
*
76+
* Inheriting classes should override this method to apply their own
77+
* update logic and applicable changes. This method assumes applicability of
78+
* the event has already been confirmed with the `isApplicable()` method.
79+
*
80+
* @param \Drupal\patternkit\Event\PatternUpdateEvent $event
81+
* The pattern update event in progress.
82+
*
83+
* @see static::onPatternUpdate()
84+
* @see static::isApplicable()
85+
*/
86+
abstract protected function doUpdate(PatternUpdateEvent $event): void;
87+
88+
}

‎src/PatternValidationTrait.php‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
namespace Drupal\patternkit;
4+
5+
use Drupal\patternkit\Entity\PatternInterface;
6+
use Drupal\patternkit\Exception\SchemaValidationException;
7+
use Drupal\patternkit\Schema\ContextBuilderTrait;
8+
use Drupal\patternkit\Schema\SchemaFactoryTrait;
9+
use Swaggest\JsonSchema\InvalidValue;
10+
11+
/**
12+
* A supporting trait for validating pattern content.
13+
*/
14+
trait PatternValidationTrait {
15+
16+
use ContextBuilderTrait;
17+
use SchemaFactoryTrait;
18+
19+
/**
20+
* An array of any exceptions encountered during validation.
21+
*
22+
* @var array<\Drupal\patternkit\Exception\SchemaValidationException>
23+
*/
24+
protected array $exceptions = [];
25+
26+
/**
27+
* Validate content being assigned to the pattern component.
28+
*
29+
* @param \Drupal\patternkit\Entity\PatternInterface $pattern
30+
* The pattern instance for content to be validated against.
31+
* @param array $content
32+
* The content values being assigned to the pattern component.
33+
*
34+
* @return bool
35+
* True if the content validates successfully. False otherwise.
36+
*/
37+
protected function validateContent(PatternInterface $pattern, array $content): bool {
38+
// Create a schema object instance with the pattern's saved schema.
39+
$schema_string = $pattern->getSchema();
40+
$schema = $this->schemaFactory()->createInstance($schema_string);
41+
42+
// Load the default context for use during validation.
43+
$context = $this->contextBuilder()->getDefaultContext();
44+
45+
try {
46+
// Feed the content into the schema object with the default context to
47+
// test for validation.
48+
$schema->in($content, $context);
49+
}
50+
catch (InvalidValue $exception) {
51+
$exception = new SchemaValidationException(
52+
message: 'Assigned content does not validate successfully.',
53+
previous: $exception,
54+
);
55+
56+
// Insert exception at the beginning to keep the most recent exceptions
57+
// first.
58+
array_unshift($this->exceptions, $exception);
59+
60+
// Return false if a validation error was encountered.
61+
return FALSE;
62+
}
63+
64+
// If we got here, no validation error was encountered so the content
65+
// should be valid.
66+
return TRUE;
67+
}
68+
69+
/**
70+
* Tests if exceptions have been encountered during validation.
71+
*
72+
* @return bool
73+
* Returns TRUE if an exception has been encountered, FALSE otherwise.
74+
*/
75+
public function hasExceptions(): bool {
76+
return !empty($this->exceptions);
77+
}
78+
79+
/**
80+
* Get an array of all exceptions that have been encountered.
81+
*
82+
* @return array<\Drupal\patternkit\Exception\SchemaValidationException>
83+
* An array of all validation exceptions that have been encountered.
84+
*/
85+
public function getExceptions(): array {
86+
return $this->exceptions;
87+
}
88+
89+
}

‎src/Schema/ContextBuilderTrait.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* A trait for accessing the ContextBuilder service and setting it for use.
77
*
88
* @see \Drupal\patternkit\Schema\ContextBuilder
9+
*
10+
* @internal
911
*/
1012
trait ContextBuilderTrait {
1113

0 commit comments

Comments
 (0)