refactor: ValueKind enum instead of kind string literals (L1)
enum ValueKind: string { Html, Code, Url, Text } with strictness() and
tightens(self $routed) (the S33a rule formerly AttrWriter/ValueCleaner::tightens
over STRICTNESS). ValueCleaner::clean() takes ?ValueKind, kindOf()/kind()/
unknownKind() return it, the sanitizer match is exhaustive over the four cases;
the "Feldart" message prints ->value, so its text is unchanged. ShortformWriter
drops the identity map CLEAN_KIND for ValueKind::tryFrom($spec['kind']) ??
ValueKind::Text; Composer and ShortformWriter pass ValueKind::Text/Url.
The installed PHPCompatibility sniffs do not know enums: two sniffs are disabled
in ValueKind.php only, with the reason.
Tests: ValueCleanerTest passes/asserts ValueKind cases instead of strings;
+ValueKindTest (full tightens matrix, case values).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -503,7 +503,7 @@ final class Composer
|
||||
}
|
||||
|
||||
try {
|
||||
$clean = $this->cleaner->clean($name, self::LABEL_PATH, $label, null, 'text');
|
||||
$clean = $this->cleaner->clean($name, self::LABEL_PATH, $label, null, ValueKind::Text);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->errors[$path] = $e->getMessage();
|
||||
|
||||
@@ -553,7 +553,7 @@ final class Composer
|
||||
|
||||
// Design-system data, but still through the choke point: an id text() would change is refused.
|
||||
try {
|
||||
$clean = $this->cleaner->clean($name, 'modulePreset', $id, null, 'text');
|
||||
$clean = $this->cleaner->clean($name, 'modulePreset', $id, null, ValueKind::Text);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$clean = null;
|
||||
}
|
||||
|
||||
@@ -26,9 +26,6 @@ final class ShortformWriter
|
||||
|
||||
private const COLOR_ERROR = 'Farbe erwartet, z. B. {{color:primary}} oder #1a1a1a';
|
||||
|
||||
/** Curated kind => sanitizer kind of ValueCleaner::clean(). */
|
||||
private const CLEAN_KIND = ['html' => 'html', 'code' => 'code', 'url' => 'url', 'text' => 'text'];
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, array<string, mixed>>>|null $modules Short form per
|
||||
* type (tests only); null = ShortformMap.
|
||||
@@ -107,7 +104,7 @@ final class ShortformWriter
|
||||
|
||||
$value = trim($this->placeholders->resolve($value));
|
||||
if (VariableRef::contains($value)) {
|
||||
$reference = $this->cleaner->clean($module, $path, $value, $subKey, 'text');
|
||||
$reference = $this->cleaner->clean($module, $path, $value, $subKey, ValueKind::Text);
|
||||
if (! is_string($reference) || VariableRef::typeOf($reference) !== 'color') {
|
||||
throw new InvalidArgumentException(self::COLOR_ERROR);
|
||||
}
|
||||
@@ -119,7 +116,7 @@ final class ShortformWriter
|
||||
throw new InvalidArgumentException(self::COLOR_ERROR);
|
||||
}
|
||||
|
||||
$clean = $this->cleaner->clean($module, $path, $value, $subKey, 'text');
|
||||
$clean = $this->cleaner->clean($module, $path, $value, $subKey, ValueKind::Text);
|
||||
if (! is_string($clean) || preg_match(self::COLOR, $clean) !== 1) {
|
||||
throw new InvalidArgumentException(self::COLOR_ERROR);
|
||||
}
|
||||
@@ -175,7 +172,7 @@ final class ShortformWriter
|
||||
|
||||
[$urlKey, $idKey, $altKey] = $keys;
|
||||
$base = $path . '.desktop.value.';
|
||||
$url = $this->cleaner->clean($module, $path, $image['url'], $urlKey, 'url');
|
||||
$url = $this->cleaner->clean($module, $path, $image['url'], $urlKey, ValueKind::Url);
|
||||
if ($url === '') {
|
||||
throw new InvalidArgumentException($notFound);
|
||||
}
|
||||
@@ -206,7 +203,7 @@ final class ShortformWriter
|
||||
}
|
||||
|
||||
try {
|
||||
$clean = $this->cleaner->clean($module, $path, $alt, $altKey, 'text');
|
||||
$clean = $this->cleaner->clean($module, $path, $alt, $altKey, ValueKind::Text);
|
||||
} catch (InvalidArgumentException) {
|
||||
return '';
|
||||
}
|
||||
@@ -253,18 +250,19 @@ final class ShortformWriter
|
||||
return $this->color($module, $path, $value, $sub);
|
||||
}
|
||||
|
||||
$kind = self::CLEAN_KIND[$spec['kind']] ?? 'text';
|
||||
// The curated kind as sanitizer kind; any other kind is text.
|
||||
$kind = ValueKind::tryFrom((string) $spec['kind']) ?? ValueKind::Text;
|
||||
if (! is_string($value) && ! is_int($value) && ! is_float($value)) {
|
||||
throw new InvalidArgumentException('Text erwartet');
|
||||
}
|
||||
|
||||
$value = (string) $value;
|
||||
if ($kind !== 'code') {
|
||||
if ($kind !== ValueKind::Code) {
|
||||
$value = $this->placeholders->resolve($value);
|
||||
}
|
||||
|
||||
$clean = $this->cleaner->clean($module, $path, $value, $sub, $kind);
|
||||
if ($kind === 'url' && $clean === '' && trim($value) !== '') {
|
||||
if ($kind === ValueKind::Url && $clean === '' && trim($value) !== '') {
|
||||
throw new InvalidArgumentException('ungültige URL');
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ use LIT\MCP\Integrations\Divi\Schema\ModuleCatalog;
|
||||
* 3. UTF-8 check of the raw string.
|
||||
* 4. S32/S32a: ControlChars::strip() on the RAW string.
|
||||
* 5. S3/S3a/S23/S24: kind routing by catalog kind, sub-field, key name, path segment
|
||||
* (kind()); S33a: a kind override may only tighten the routed kind (tightens()).
|
||||
* (kind(), a ValueKind); S33a: a kind override may only tighten the routed kind
|
||||
* (ValueKind::tightens()).
|
||||
* 6. S1/S2: a string with the $variable( marker → VariableRef::canonical() (whole value only;
|
||||
* URL fields only content references) – its result is returned as it is.
|
||||
* 7. the sanitizer of the kind: html / code / url / text (ValueSanitizer).
|
||||
@@ -48,9 +49,6 @@ final class ValueCleaner
|
||||
/** Last path segment naming a URL when neither kind nor key does (ruling S24): redirectUrl. */
|
||||
private const URL_SEGMENT = '/url$/i';
|
||||
|
||||
/** Text-family kinds from laxest to strictest (ruling S33a); url stands apart. */
|
||||
private const STRICTNESS = ['code' => 0, 'html' => 1, 'text' => 2];
|
||||
|
||||
/**
|
||||
* Raw HTML/CSS fields, and the ones Divi re-serializes whole pages for (rulings S7/S26), by
|
||||
* path shape at any depth: module.advanced.html(.htmlBefore), module.decoration.attributes,
|
||||
@@ -68,12 +66,11 @@ final class ValueCleaner
|
||||
/**
|
||||
* Cleans one value written to $path (steps 1–8 above). $subKey names the sub-field of a
|
||||
* composite path ("linkUrl" of button.innerContent) when $value is its string alone. $kind
|
||||
* ('html'|'code'|'url'|'text') replaces the catalog routing for every string of $value: the
|
||||
* short form passes its curated field kind (ShortformMap), so icon-list-item text is always
|
||||
* plain text. It may only tighten (ruling S33a): code < html < text; url only where the
|
||||
* routing says url – a weaker or other-family kind is refused.
|
||||
* replaces the catalog routing for every string of $value: the short form passes its
|
||||
* curated field kind (ShortformMap), so icon-list-item text is always plain text. It may only
|
||||
* tighten (ruling S33a): code < html < text; url only where the routing says url – a weaker
|
||||
* or other-family kind is refused.
|
||||
*
|
||||
* @param 'html'|'code'|'url'|'text'|null $kind
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function clean(
|
||||
@@ -81,7 +78,7 @@ final class ValueCleaner
|
||||
string $path,
|
||||
mixed $value,
|
||||
?string $subKey = null,
|
||||
?string $kind = null
|
||||
?ValueKind $kind = null
|
||||
): mixed {
|
||||
$this->guardPath($path);
|
||||
|
||||
@@ -91,10 +88,8 @@ final class ValueCleaner
|
||||
/**
|
||||
* The kind clean() routes a string of $path (sub-field $subKey) to. lit/divi-replace-text
|
||||
* uses it to leave URL and code fields alone (D10).
|
||||
*
|
||||
* @return 'html'|'code'|'url'|'text'
|
||||
*/
|
||||
public function kindOf(string $module, string $path, ?string $subKey = null): string
|
||||
public function kindOf(string $module, string $path, ?string $subKey = null): ValueKind
|
||||
{
|
||||
return $this->kind($module, $path, $subKey, $subKey);
|
||||
}
|
||||
@@ -111,7 +106,7 @@ final class ValueCleaner
|
||||
return function (string $path, ?string $key, string $value) use ($module): ?string {
|
||||
$kind = $this->kindOf($module, $path, $key);
|
||||
|
||||
return $kind === 'text' || $kind === 'html'
|
||||
return $kind === ValueKind::Text || $kind === ValueKind::Html
|
||||
? (string) $this->clean($module, $path, $value, $key)
|
||||
: null;
|
||||
};
|
||||
@@ -139,7 +134,7 @@ final class ValueCleaner
|
||||
*
|
||||
* @param string|null $subName First key below the state value: selects the sub-kind of a composite path.
|
||||
* @param string|null $key Nearest key above $value: the URL fallback when the catalog names no kind.
|
||||
* @param 'html'|'code'|'url'|'text'|null $kind Fixed kind instead of the routing (short form).
|
||||
* @param ValueKind|null $kind Fixed kind instead of the routing (short form).
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function cleanNode(
|
||||
@@ -148,7 +143,7 @@ final class ValueCleaner
|
||||
mixed $value,
|
||||
?string $subName,
|
||||
?string $key,
|
||||
?string $kind = null
|
||||
?ValueKind $kind = null
|
||||
): mixed {
|
||||
if (is_array($value)) {
|
||||
$clean = [];
|
||||
@@ -185,27 +180,27 @@ final class ValueCleaner
|
||||
|
||||
// Step 5 (S3/S3a/S23/S24, S33a).
|
||||
$routed = $this->kind($module, $path, $subName, $key);
|
||||
if ($kind !== null && ! self::tightens($kind, $routed)) {
|
||||
if ($kind !== null && ! $kind->tightens($routed)) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'Feldart "%s" ist für "%s" nicht zulässig (das Attribut verlangt "%s").',
|
||||
$kind,
|
||||
$kind->value,
|
||||
$path,
|
||||
$routed
|
||||
$routed->value
|
||||
));
|
||||
}
|
||||
|
||||
$kind ??= $routed;
|
||||
// Step 6 (S1/S2): the only way a reference is stored – whole, straight from canonical().
|
||||
if (VariableRef::contains($value)) {
|
||||
return $this->references->canonical($value, $this->sanitizer, $kind === 'url');
|
||||
return $this->references->canonical($value, $this->sanitizer, $kind === ValueKind::Url);
|
||||
}
|
||||
|
||||
// Step 7.
|
||||
$clean = match ($kind) {
|
||||
'html' => $this->sanitizer->html($value),
|
||||
'code' => $this->sanitizer->code($value),
|
||||
'url' => $this->sanitizer->url($value),
|
||||
default => $this->sanitizer->text($value),
|
||||
ValueKind::Html => $this->sanitizer->html($value),
|
||||
ValueKind::Code => $this->sanitizer->code($value),
|
||||
ValueKind::Url => $this->sanitizer->url($value),
|
||||
ValueKind::Text => $this->sanitizer->text($value),
|
||||
};
|
||||
|
||||
// Step 8 (S28): sanitizers strip characters ("<x>", "%41") and can thereby build a marker
|
||||
@@ -218,31 +213,14 @@ final class ValueCleaner
|
||||
return $clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the kind override $kind is at least as strict as the routed kind (ruling S33a).
|
||||
* Strictness within the text family: code (raw for unfiltered_html) < html (kses) < text
|
||||
* (no markup). url (esc_url_raw) is a family of its own: it neither tightens nor is
|
||||
* tightened by the others, so only url → url passes.
|
||||
*/
|
||||
private static function tightens(string $kind, string $routed): bool
|
||||
{
|
||||
if ($kind === 'url' || $routed === 'url') {
|
||||
return $kind === $routed;
|
||||
}
|
||||
|
||||
return self::STRICTNESS[$kind] >= self::STRICTNESS[$routed];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizer for a string (rulings S3/S3a/S23/S24): the catalog kind of the path – or of the
|
||||
* sub-field for a composite path – decides. A string reached through a key below a path
|
||||
* with one scalar kind counts as unknown (S24). Unknown falls back to the key name (URL keys
|
||||
* at any depth), then to the last path segment (…Url), then content.innerContent → rich
|
||||
* text (S23: fields Divi defines only in JS), else text – never laxer than text.
|
||||
*
|
||||
* @return 'html'|'code'|'url'|'text'
|
||||
*/
|
||||
private function kind(string $module, string $path, ?string $subName, ?string $key): string
|
||||
private function kind(string $module, string $path, ?string $subName, ?string $key): ValueKind
|
||||
{
|
||||
// "unknown" also for a composite path whose sub-kinds do not name $subName.
|
||||
$kind = $this->catalog->pathKind($module, $path, $subName);
|
||||
@@ -251,26 +229,26 @@ final class ValueCleaner
|
||||
}
|
||||
|
||||
return match ($kind) {
|
||||
'richtext' => 'html',
|
||||
'code' => 'code',
|
||||
'url', 'image' => 'url',
|
||||
'richtext' => ValueKind::Html,
|
||||
'code' => ValueKind::Code,
|
||||
'url', 'image' => ValueKind::Url,
|
||||
'unknown' => $this->unknownKind($path, $key),
|
||||
default => 'text',
|
||||
default => ValueKind::Text,
|
||||
};
|
||||
}
|
||||
|
||||
/** @return 'html'|'url'|'text' */
|
||||
private function unknownKind(string $path, ?string $key): string
|
||||
/** Html, Url or Text – never Code. */
|
||||
private function unknownKind(string $path, ?string $key): ValueKind
|
||||
{
|
||||
if ($key !== null) {
|
||||
return preg_match(self::URL_KEY, $key) === 1 ? 'url' : 'text';
|
||||
return preg_match(self::URL_KEY, $key) === 1 ? ValueKind::Url : ValueKind::Text;
|
||||
}
|
||||
|
||||
$segments = explode('.', $path);
|
||||
if (preg_match(self::URL_SEGMENT, (string) end($segments)) === 1) {
|
||||
return 'url';
|
||||
return ValueKind::Url;
|
||||
}
|
||||
|
||||
return $path === 'content.innerContent' ? 'html' : 'text';
|
||||
return $path === 'content.innerContent' ? ValueKind::Html : ValueKind::Text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Compose;
|
||||
|
||||
// The installed PHPCompatibility sniffs predate enums (PHP 8.1) and read their methods as plain functions.
|
||||
// phpcs:disable PHPCompatibility.Variables.ForbiddenThisUseContexts.OutsideObjectContext
|
||||
// phpcs:disable PHPCompatibility.FunctionDeclarations.NewParamTypeDeclarations.SelfOutsideClassScopeFound
|
||||
|
||||
/**
|
||||
* The sanitizer kind ValueCleaner routes a string to (step 5 of its pipeline): html (kses for
|
||||
* users without unfiltered_html), code (raw only with unfiltered_html), url (esc_url_raw) or
|
||||
* text (no markup). Pure PHP.
|
||||
*/
|
||||
enum ValueKind: string
|
||||
{
|
||||
case Html = 'html';
|
||||
case Code = 'code';
|
||||
case Url = 'url';
|
||||
case Text = 'text';
|
||||
|
||||
/** Position in the text family from laxest to strictest (ruling S33a); null for url, which stands apart. */
|
||||
public function strictness(): ?int
|
||||
{
|
||||
return match ($this) {
|
||||
self::Code => 0,
|
||||
self::Html => 1,
|
||||
self::Text => 2,
|
||||
self::Url => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this kind, as an override, is at least as strict as the routed kind (ruling S33a).
|
||||
* Strictness within the text family: code (raw for unfiltered_html) < html (kses) < text
|
||||
* (no markup). url (esc_url_raw) is a family of its own: it neither tightens nor is
|
||||
* tightened by the others, so only url → url passes.
|
||||
*/
|
||||
public function tightens(self $routed): bool
|
||||
{
|
||||
if ($this === self::Url || $routed === self::Url) {
|
||||
return $this === $routed;
|
||||
}
|
||||
|
||||
return $this->strictness() >= $routed->strictness();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LIT\MCP\Integrations\Divi\Compose\ValueKind;
|
||||
use LIT\MCP\Tests\Support\ComposeFixture;
|
||||
use LIT\MCP\Tests\Support\DiviFixtures;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -100,13 +101,13 @@ final class ValueCleanerTest extends TestCase
|
||||
{
|
||||
$cleaner = ComposeFixture::cleaner();
|
||||
|
||||
self::assertSame('html', $cleaner->kindOf('divi/text', 'content.innerContent'));
|
||||
self::assertSame('text', $cleaner->kindOf('divi/heading', 'title.innerContent'));
|
||||
self::assertSame('code', $cleaner->kindOf('divi/code', 'content.innerContent'));
|
||||
self::assertSame('text', $cleaner->kindOf('divi/fullwidth-header', 'buttonOne.innerContent', 'text'));
|
||||
self::assertSame('url', $cleaner->kindOf('divi/fullwidth-header', 'buttonOne.innerContent', 'linkUrl'));
|
||||
self::assertSame('text', $cleaner->kindOf('divi/image', 'image.innerContent', 'alt'));
|
||||
self::assertSame('url', $cleaner->kindOf('divi/image', 'image.innerContent', 'src'));
|
||||
self::assertSame(ValueKind::Html, $cleaner->kindOf('divi/text', 'content.innerContent'));
|
||||
self::assertSame(ValueKind::Text, $cleaner->kindOf('divi/heading', 'title.innerContent'));
|
||||
self::assertSame(ValueKind::Code, $cleaner->kindOf('divi/code', 'content.innerContent'));
|
||||
self::assertSame(ValueKind::Text, $cleaner->kindOf('divi/fullwidth-header', 'buttonOne.innerContent', 'text'));
|
||||
self::assertSame(ValueKind::Url, $cleaner->kindOf('divi/fullwidth-header', 'buttonOne.innerContent', 'linkUrl'));
|
||||
self::assertSame(ValueKind::Text, $cleaner->kindOf('divi/image', 'image.innerContent', 'alt'));
|
||||
self::assertSame(ValueKind::Url, $cleaner->kindOf('divi/image', 'image.innerContent', 'src'));
|
||||
}
|
||||
|
||||
/** Ruling S24: keys below a scalar-kind path are unknown; a …Url path segment means URL. */
|
||||
@@ -163,19 +164,19 @@ final class ValueCleanerTest extends TestCase
|
||||
{
|
||||
$cleaner = ComposeFixture::cleaner();
|
||||
|
||||
self::assertSame('x', $cleaner->clean('divi/text', 'content.innerContent', '<p>x</p>', null, 'text'));
|
||||
self::assertSame('x', $cleaner->clean('divi/code', 'content.innerContent', '<b>x</b>', null, 'text'));
|
||||
self::assertSame('<p>x</p>', $cleaner->clean('divi/code', 'content.innerContent', '<p>x</p><script>y</script>', null, 'html'));
|
||||
self::assertSame('Los', $cleaner->clean('divi/heading', 'title.innerContent', '<b>Los</b>', null, 'text'));
|
||||
self::assertSame('/a', $cleaner->clean('divi/button', 'button.innerContent', '/a', 'linkUrl', 'url'));
|
||||
self::assertSame('x', $cleaner->clean('divi/text', 'content.innerContent', '<p>x</p>', null, ValueKind::Text));
|
||||
self::assertSame('x', $cleaner->clean('divi/code', 'content.innerContent', '<b>x</b>', null, ValueKind::Text));
|
||||
self::assertSame('<p>x</p>', $cleaner->clean('divi/code', 'content.innerContent', '<p>x</p><script>y</script>', null, ValueKind::Html));
|
||||
self::assertSame('Los', $cleaner->clean('divi/heading', 'title.innerContent', '<b>Los</b>', null, ValueKind::Text));
|
||||
self::assertSame('/a', $cleaner->clean('divi/button', 'button.innerContent', '/a', 'linkUrl', ValueKind::Url));
|
||||
|
||||
$weaker = [
|
||||
'html over text' => ['divi/heading', 'title.innerContent', null, 'html'],
|
||||
'code over text' => ['divi/heading', 'title.innerContent', null, 'code'],
|
||||
'code over html' => ['divi/text', 'content.innerContent', null, 'code'],
|
||||
'url over text' => ['divi/heading', 'title.innerContent', null, 'url'],
|
||||
'text over url' => ['divi/button', 'button.innerContent', 'linkUrl', 'text'],
|
||||
'html over url' => ['divi/button', 'button.innerContent', 'linkUrl', 'html'],
|
||||
'html over text' => ['divi/heading', 'title.innerContent', null, ValueKind::Html],
|
||||
'code over text' => ['divi/heading', 'title.innerContent', null, ValueKind::Code],
|
||||
'code over html' => ['divi/text', 'content.innerContent', null, ValueKind::Code],
|
||||
'url over text' => ['divi/heading', 'title.innerContent', null, ValueKind::Url],
|
||||
'text over url' => ['divi/button', 'button.innerContent', 'linkUrl', ValueKind::Text],
|
||||
'html over url' => ['divi/button', 'button.innerContent', 'linkUrl', ValueKind::Html],
|
||||
];
|
||||
foreach ($weaker as $name => [$module, $path, $sub, $kind]) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Compose;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Compose\ValueKind;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/** Ruling S33a: an override may only tighten – code < html < text; url only to url. */
|
||||
final class ValueKindTest extends TestCase
|
||||
{
|
||||
public function testTightensMatrix(): void
|
||||
{
|
||||
$allowed = [
|
||||
'code' => ['code'],
|
||||
'html' => ['code', 'html'],
|
||||
'text' => ['code', 'html', 'text'],
|
||||
'url' => ['url'],
|
||||
];
|
||||
foreach (ValueKind::cases() as $override) {
|
||||
foreach (ValueKind::cases() as $routed) {
|
||||
self::assertSame(
|
||||
in_array($routed->value, $allowed[$override->value], true),
|
||||
$override->tightens($routed),
|
||||
$override->value . ' over ' . $routed->value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testValuesAreTheCuratedKindNames(): void
|
||||
{
|
||||
self::assertSame(['html', 'code', 'url', 'text'], array_map(static fn (ValueKind $kind): string => $kind->value, ValueKind::cases()));
|
||||
self::assertNull(ValueKind::tryFrom('richtext'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user