TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:15 +02:00
commit 6830982e7d
295 changed files with 31995 additions and 0 deletions
+836
View File
@@ -0,0 +1,836 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Utility;
use TYPO3\CMS\Core\Core\RequestId;
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Directive;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface;
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager;
use TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* This class is a backport of the corresponding class of TYPO3 Flow.
* All credits go to the TYPO3 Flow team.
*
* A debugging utility class
*/
class DebuggerUtility
{
public const PLAINTEXT_INDENT = ' ';
/**
* @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage
*/
protected static $renderedObjects;
/**
* Hardcoded list of Extbase class names (regex) which should not be displayed during debugging
*
* @var array
*/
protected static $blacklistedClassNames = [
ReflectionService::class,
DataMapper::class,
PersistenceManager::class,
QueryObjectModelFactory::class,
ContentObjectRenderer::class,
];
/**
* Hardcoded list of property names (regex) which should not be displayed during debugging
*
* @var array
*/
protected static $blacklistedPropertyNames = ['warning'];
/**
* Is set to TRUE once the CSS file is included in the current page to prevent double inclusions of the CSS file.
*
* @var bool
*/
protected static $stylesheetEchoed = false;
/**
* Defines the max recursion depth of the dump, set to 8 due to common memory limits
*
* @var int
*/
protected static $maxDepth = 8;
/**
* Clear the state of the debugger
*/
protected static function clearState(): void
{
self::$renderedObjects = new ObjectStorage();
}
/**
* Renders a dump of the given value
*/
protected static function renderDump($value, int $level, bool $plainText, bool $ansiColors, array $headerPrefix = []): array|callable|string
{
$dump = [];
if (is_string($value)) {
$croppedValue = mb_strlen($value) > 2000 ? mb_substr($value, 0, 2000) . '...' : $value;
if ($plainText) {
$dump = [
self::ansiEscapeWrap('"' . implode(PHP_EOL . str_repeat(self::PLAINTEXT_INDENT, $level + 1), mb_str_split($croppedValue, 76)) . '"', '33', $plainText, $ansiColors),
' (',
mb_strlen($value),
' chars)',
];
} else {
$lines = mb_str_split($croppedValue, 76);
$content = [];
foreach ($lines as $key => $line) {
if ($key > 0) {
$content[] = self::html('br', []);
$content[] = static fn(): string => '&nbsp;';
}
$content[] = $line;
}
$dump = self::html('span', ['class' => 'extbase-debug-string-container'], [
'\'',
self::html('span', ['class' => 'extbase-debug-string'], $content),
'\' (',
mb_strlen($value),
' chars)',
]);
}
} elseif (is_numeric($value)) {
$dump = [
self::ansiEscapeWrap((string)$value, '35', $plainText, $ansiColors),
' (',
gettype($value),
')',
];
} elseif (is_bool($value)) {
$dump = $value ? self::ansiEscapeWrap('TRUE', '32', $plainText, $ansiColors) : self::ansiEscapeWrap('FALSE', '32', $plainText, $ansiColors);
} elseif ($value === null || is_resource($value)) {
$dump = gettype($value);
} elseif (is_array($value)) {
return self::renderArray($value, $level + 1, $plainText, $ansiColors, $headerPrefix);
} elseif (is_object($value)) {
if ($value instanceof \Closure) {
return self::renderClosure($value, $level + 1, $plainText, $ansiColors, $headerPrefix);
}
return self::renderObject($value, $level + 1, $plainText, $ansiColors, $headerPrefix);
}
if ($plainText) {
return [$headerPrefix, $dump];
}
return self::html('div', ['class' => 'extbase-debug-header'], [$headerPrefix, $dump]);
}
/**
* Renders a dump of the given array
*/
protected static function renderArray(array $array, int $level, bool $plainText = false, bool $ansiColors = false, array $headerPrefix = []): array|callable
{
$content = [];
$count = count($array);
$header = $headerPrefix;
$header[] = self::styled('', 'expander', $plainText, $ansiColors);
$header[] = self::styled('array', 'type', $plainText, $ansiColors, 0, 1);
$header[] = $count > 0 ? '(' . $count . ' item' . ($count > 1 ? 's' : '') . ')' : '(empty)';
if ($level >= self::$maxDepth) {
$header[] = self::styled('max depth', 'filtered', $plainText, $ansiColors, 1);
} else {
$content = self::renderCollection($array, $level, $plainText, $ansiColors);
}
if ($plainText) {
return [...$header, ...$content];
}
if (array_filter($content) === []) {
return self::html('div', ['class' => 'extbase-debug-header'], $header);
}
return self::html('details', ['class' => 'extbase-debugger-tree', 'open' => $level > 1 && $count > 0 ? null : ''], [
self::html('summary', ['class' => 'extbase-debug-header'], $header),
self::html('div', ['class' => 'extbase-debug-content'], $content),
]);
}
/**
* Renders a dump of the given object
*/
protected static function renderObject(object $object, int $level, bool $plainText = false, bool $ansiColors = false, array $headerPrefix = []): array|callable
{
if ($object instanceof LazyLoadingProxy) {
$object = $object->_loadRealInstance();
if (!is_object($object)) {
return [...$headerPrefix, gettype($object)];
}
}
$header = self::renderHeader($object, $level, $plainText, $ansiColors, $headerPrefix);
$content = [];
if ($level < self::$maxDepth && !self::isBlacklisted($object) && !(self::isAlreadyRendered($object) && $plainText !== true)) {
$content = self::renderContent($object, $level, $plainText, $ansiColors);
}
if ($plainText) {
return [...$header, ...$content];
}
if (array_filter($content) === []) {
return self::html('div', ['class' => 'extbase-debugger-header'], $header);
}
return self::html('details', ['class' => 'extbase-debugger-tree', 'open' => $level > 1 ? null : ''], [
self::html('summary', ['class' => 'extbase-debug-header', 'id' => spl_object_hash($object)], $header),
self::html('div', ['class' => 'extbase-debug-content'], $content),
]);
}
/**
* Renders a dump of the given closure
*/
protected static function renderClosure(\Closure $object, int $level, bool $plainText = false, bool $ansiColors = false, array $headerPrefix = []): array|callable
{
$header = self::renderHeader($object, $level, $plainText, $ansiColors, $headerPrefix);
$content = [];
if ($level < self::$maxDepth && (!self::isAlreadyRendered($object) || $plainText)) {
$content = self::renderContent($object, $level, $plainText, $ansiColors);
}
if ($plainText) {
return [...$header, ...$content];
}
if (array_filter($content) === []) {
return self::html('div', ['class' => 'extbase-debugger-header'], $header);
}
return self::html('details', ['class' => 'extbase-debugger-tree', 'open' => $level > 1 ? null : ''], [
self::html('summary', ['class' => 'extbase-debug-header'], $header),
self::html('div', ['class' => 'extbase-debug-content extbase-debug-closure'], $content),
]);
}
/**
* Checks if a given object or property should be excluded/filtered
*
* @param object $value A ReflectionProperty or other Object
* @return bool TRUE if the given object should be filtered
*/
protected static function isBlacklisted(object $value): bool
{
if ($value instanceof \ReflectionProperty) {
$result = in_array($value->getName(), self::$blacklistedPropertyNames, true);
} else {
$result = in_array(get_class($value), self::$blacklistedClassNames, true);
}
return $result;
}
/**
* Checks if a given object was already rendered.
*
* @return bool TRUE if the given object was already rendered
*/
protected static function isAlreadyRendered(object $object): bool
{
return self::$renderedObjects->contains($object);
}
/**
* Renders the header of a given object/collection. It is usually the class name along with some flags.
*
* @return array<string|callable> string The rendered header with tags
*/
protected static function renderHeader(object $object, int $level, bool $plainText, bool $ansiColors, array $headerPrefix): array
{
$dump = $headerPrefix;
$persistenceType = null;
$className = get_class($object);
$classReflection = new \ReflectionClass($className);
$dump[] = self::styled('', 'expander', $plainText, $ansiColors);
$dump[] = self::styled($className, 'type', $plainText, $ansiColors);
if (!$object instanceof \Closure) {
if ($object instanceof SingletonInterface) {
$scope = 'singleton';
} else {
$scope = 'prototype';
}
$dump[] = self::styled($scope, 'scope', $plainText, $ansiColors, 1);
if ($object instanceof DomainObjectInterface) {
if ($object->_isDirty()) {
$persistenceType = 'modified';
} elseif ($object->_isNew()) {
$persistenceType = 'transient';
} else {
$persistenceType = 'persistent';
}
}
if ($object instanceof ObjectStorage && $object->_isDirty()) {
$persistenceType = 'modified';
}
if ($object instanceof AbstractEntity) {
$domainObjectType = 'entity';
} elseif ($object instanceof AbstractValueObject) {
$domainObjectType = 'valueobject';
} else {
$domainObjectType = 'object';
}
$persistenceType = $persistenceType === null ? '' : $persistenceType . ' ';
$dump[] = self::styled($persistenceType . $domainObjectType, 'ptype', $plainText, $ansiColors, 1);
}
if (strpos(implode('|', self::$blacklistedClassNames), get_class($object)) > 0) {
$dump[] = self::styled('filtered', 'filtered', $plainText, $ansiColors, 1);
} elseif (self::$renderedObjects->contains($object) && !$plainText) {
$dump = [
self::html('a', ['href' => '#' . spl_object_hash($object), 'class' => 'extbase-debug-seeabove'], [
$dump,
self::html('span', ['class' => 'extbase-debug-filtered'], 'see above'),
]),
];
} elseif ($level >= self::$maxDepth && !$object instanceof \DateTimeInterface) {
$dump[] = self::styled('max depth', 'filtered', $plainText, $ansiColors, 1);
}
if ($object instanceof \Countable) {
$objectCount = count($object);
$dump[] = $objectCount > 0 ? ' (' . $objectCount . ' items)' : ' (empty)';
}
if ($object instanceof \DateTimeInterface) {
$dump[] = ' (' . $object->format(\DateTimeInterface::RFC3339) . ', ' . $object->getTimestamp() . ')';
}
if ($object instanceof DomainObjectInterface && !$object->_isNew()) {
$dump[] = ' (uid=' . $object->getUid() . ', pid=' . $object->getPid() . ')';
}
return $dump;
}
protected static function renderContent(object $object, int $level, bool $plainText, bool $ansiColors): string|array
{
$dump = [];
if ($object instanceof \Iterator || $object instanceof \ArrayObject) {
$dump[] = self::renderCollection($object, $level, $plainText, $ansiColors);
} else {
self::$renderedObjects->attach($object);
if ($object instanceof \Closure) {
if ($plainText) {
$dump[] = PHP_EOL;
}
$dump[] = str_repeat(self::PLAINTEXT_INDENT, $level);
$dump[] = self::styled('function (', 'closure', $plainText, $ansiColors);
$reflectionFunction = new \ReflectionFunction($object);
$params = [];
$count = 0;
foreach ($reflectionFunction->getParameters() as $parameter) {
if (++$count > 1) {
$params[] = ', ';
}
$isFirst = false;
$type = $parameter->getType();
// @todo Following code adds for parameter of type array or a class the classname or array
// to the output. All other introduced possible parameter types are not respected yet.
// This should be extended, and also respect possible type combinations like
// union types and union intersect types.
if ($type instanceof \ReflectionNamedType && $type->isBuiltin() && $type->getName() === 'array') {
$params[] = self::styled('array', 'type', $plainText, $ansiColors, 0, 1);
} elseif ($type instanceof \ReflectionNamedType && !$type->isBuiltin() && !empty($type->getName())) {
$params[] = self::styled($type->getName(), 'type', $plainText, $ansiColors, 0, 1);
}
if ($parameter->isPassedByReference()) {
$params[] = '&';
}
if ($parameter->isVariadic()) {
$params[] = '...';
}
$params[] = self::styled('$' . $parameter->name, 'property', $plainText, $ansiColors);
if ($parameter->isDefaultValueAvailable()) {
$params[] = ' = ';
$params[] = self::styled(var_export($parameter->getDefaultValue(), true), 'string', $plainText, $ansiColors);
}
}
$dump = [...$dump, ...$params];
$dump[] = self::styled(') {', 'closure', $plainText, $ansiColors);
$dump[] = PHP_EOL;
$lines = (array)file((string)$reflectionFunction->getFileName());
for ($l = (int)$reflectionFunction->getStartLine(); $l < (int)$reflectionFunction->getEndLine() - 1; ++$l) {
$line = (string)($lines[$l] ?? '');
$dump[] = $line;
}
$dump[] = str_repeat(self::PLAINTEXT_INDENT, $level);
$dump[] = self::styled('}', 'closure', $plainText, $ansiColors);
if ($plainText) {
$dump[] = PHP_EOL;
}
} else {
if (get_class($object) === \stdClass::class) {
$objReflection = new \ReflectionObject($object);
$properties = $objReflection->getProperties();
} else {
$classReflection = new \ReflectionClass(get_class($object));
$properties = $classReflection->getProperties();
}
foreach ($properties as $property) {
if (self::isBlacklisted($property)) {
continue;
}
$visibility = ($property->isProtected() ? 'protected' : ($property->isPrivate() ? 'private' : 'public'));
$header = [
PHP_EOL . str_repeat(self::PLAINTEXT_INDENT, $level),
self::styled($property->getName(), 'property', $plainText, $ansiColors),
' => ',
self::styled($visibility, 'visibility', $plainText, $ansiColors, 0, 1),
];
if (!$property->isInitialized($object)) {
$header[] = self::styled('uninitialized', 'uninitialized', $plainText, $ansiColors, 0, 1);
if ($plainText) {
$dump[] = $header;
} else {
$dump[] = self::html('div', ['class' => 'extbase-debug-header'], $header);
}
continue;
}
if ($object instanceof DomainObjectInterface && !$object->_isNew() && $object->_isDirty($property->getName())) {
$header[] = self::styled('modified', 'dirty', $plainText, $ansiColors, 1);
}
$dump[] = self::renderDump($property->getValue($object), $level, $plainText, $ansiColors, $header);
}
}
}
return $dump;
}
protected static function renderCollection(iterable $collection, int $level, bool $plainText, bool $ansiColors): array
{
$dump = [];
foreach ($collection as $key => $value) {
// Note: Due to the TYPO3\CMS\Core\Type\Map implementation, the key can also be an object.
$key = is_object($key) ? get_class($key) : (string)$key;
$dump = [
...$dump,
self::renderDump($value, $level, $plainText, $ansiColors, [
PHP_EOL . str_repeat(self::PLAINTEXT_INDENT, $level),
self::styled($key, 'property', $plainText, $ansiColors),
' => ',
]),
];
}
if ($collection instanceof \Iterator && !$collection instanceof \Generator) {
$collection->rewind();
}
return $dump;
}
/**
* Wrap a string with the ANSI escape sequence for colorful output
*
* @param string $string The string to wrap
* @param string $ansiColorSequence The ansi color sequence (e.g. "1;37")
* @param bool $plainText If FALSE, the string will be HTML encoded
* @param bool $ansiColors If TRUE, the string will have console colors applied
* @return callable The wrapped or raw string
*/
protected static function ansiEscapeWrap(string $string, string $ansiColorSequence, bool $plainText, bool $ansiColors): callable
{
if ($plainText && $ansiColors) {
return static fn(): string => '[' . $ansiColorSequence . 'm' . self::escapeConsoleText($string) . '';
}
if ($plainText) {
return static fn(): string => self::escapeConsoleText($string);
}
return static fn(): string => self::escapeHtml($string);
}
/**
* A var_dump function optimized for Extbase's object structures
*
* @param mixed $variable The value to dump
* @param string $title optional custom title for the debug output
* @param int $maxDepth Sets the max recursion depth of the dump. De- or increase the number according to your needs and memory limit.
* @param bool $plainText If TRUE, the dump is in plain text, if FALSE the debug output is in HTML format.
* @param bool $ansiColors If TRUE (default), ANSI color codes is added to the output, if FALSE the debug output not colored.
* @param bool $return if TRUE, the dump is returned for custom post-processing (e.g. embed in custom HTML). If FALSE (default), the dump is directly displayed.
* @param array $blacklistedClassNames An array of class names (RegEx) to be filtered. Default is an array of some common class names.
* @param array $blacklistedPropertyNames An array of property names and/or array keys (RegEx) to be filtered. Default is an array of some common property names.
* @return string if $return is TRUE, the dump is returned. By default, the dump is directly displayed, and nothing is returned.
*/
public static function var_dump(
$variable,
?string $title = null,
int $maxDepth = 8,
bool $plainText = false,
bool $ansiColors = true,
bool $return = false,
?array $blacklistedClassNames = null,
?array $blacklistedPropertyNames = null
): string {
self::$maxDepth = $maxDepth;
if ($title === null) {
$title = 'Extbase Variable Dump';
}
$ansiColors = $plainText && $ansiColors;
if ($ansiColors === true) {
$title = '' . $title . '';
}
$backupBlacklistedClassNames = self::$blacklistedClassNames;
if (is_array($blacklistedClassNames)) {
self::$blacklistedClassNames = $blacklistedClassNames;
}
$backupBlacklistedPropertyNames = self::$blacklistedPropertyNames;
if (is_array($blacklistedPropertyNames)) {
self::$blacklistedPropertyNames = $blacklistedPropertyNames;
}
self::clearState();
$css = self::cssTreeToString([
'.extbase-debugger-tree' => [
'position' => 'relative',
],
'.extbase-debugger-tree summary' => [
'list-style' => 'none',
'cursor' => 'pointer',
'white-space' => 'nowrap',
],
'.extbase-debugger-tree:has(>summary:target)' => [
'outline' => 'var(--typo3-debugger-outline, #101010) auto 1px',
'padding' => '3px',
],
'.extbase-debugger-tree :is(.extbase-debug-header)>*' => [
'vertical-align' => 'top',
],
'.extbase-debugger-tree .extbase-debug-expander' => [
'position' => 'relative',
'display' => 'none',
'height' => '1em',
'aspect-ratio' => '1',
'margin' => '0 3px 0 0',
'vertical-align' => '-12%',
'cursor' => 'pointer',
],
'.extbase-debugger-tree summary>.extbase-debug-expander' => [
'display' => 'inline-block',
],
// Hide expander on first level
'.extbase-debugger-inner>details>summary>.extbase-debug-expander' => [
'display' => 'none',
],
'.extbase-debugger-tree .extbase-debug-expander::before' => [
'content' => '""',
'position' => 'absolute',
'inset' => '0',
'background-size' => '100%',
'display' => 'inline-block',
'background-image' => sprintf('url(data:image/svg+xml;base64,%s)', base64_encode(
'<?xml version="1.0" encoding="utf-8"?><svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="#888"><path d="M11,11H0V0h11V11z M10,1H1v9h9V1z"/><rect x="2" y="5" width="7" height="1"/><rect x="5" y="2" width="1" height="7"/></svg>'
)),
],
'.extbase-debugger-tree[open]>summary>.extbase-debug-expander::before' => [
'background-image' => sprintf('url(data:image/svg+xml;base64,%s)', base64_encode(
'<?xml version="1.0" encoding="utf-8"?><svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="#888"><path d="M11,11H0V0h11V11z M10,1H1v9h9V1z"/><rect x="2" y="5" width="7" height="1"/></svg>'
)),
],
'.extbase-debugger-tree .extbase-debug-content' => [
'padding-left' => '3ch',
],
'.extbase-debugger' => [
'display' => 'block',
'text-align' => 'left',
'background' => 'var(--typo3-debugger-bg, #2a2a2a)',
'border' => '1px solid var(--typo3-debugger-border-color, #2a2a2a)',
'box-shadow' => 'var(--typo3-debugger-box-shadow, 0 3px 0 rgba(0, 0, 0, .5))',
'margin' => '20px',
'overflow' => 'hidden',
'border-radius' => 'var(--typo3-debugger-border-radius, 4px)',
],
'.extbase-debugger-floating' => [
'position' => 'relative',
'z-index' => '99990',
],
'.extbase-debugger-top' => [
'background' => 'var(--typo3-debugger-top-bg, #444)',
'font-size' => '12px',
'font-family' => 'monospace',
'color' => 'var(--typo3-debugger-top-color, #f1f1f1)',
'padding' => '6px 15px',
],
'.extbase-debugger-inner' => [
'overflow-x' => 'auto',
],
'.extbase-debugger-center' => [
'padding' => '0 15px',
'margin' => '15px 0',
'background-image' => sprintf(
'repeating-linear-gradient(to bottom, transparent 0, transparent 20px, %1$s 20px, %1$s 40px)',
'var(--typo3-debugger-bg-variant, #252525)',
),
'color' => 'var(--typo3-debugger-color-variant, #999)',
'word-wrap' => 'break-word',
],
'.extbase-debugger-center, .extbase-debugger-center :is(.extbase-debug-string, a, p, pre, strong)' => [
'font-size' => '12px',
'font-weight' => '400',
'font-family' => 'monospace',
'line-height' => '20px',
'color' => 'var(--typo3-debugger-color, #f1f1f1)',
],
'.extbase-debugger-center .extbase-debug-string-container' => [
'display' => 'inline-block',
],
'.extbase-debugger-center :is(.extbase-debug-filtered, .extbase-debug-proxy, .extbase-debug-ptype, .extbase-debug-visibility, .extbase-debug-uninitialized, .extbase-debug-scope, .extbase-debug-dirty)' => [
'color' => '#fff',
'font-size' => '10px',
'line-height' => '18px',
'padding' => '2px 4px',
'margin-right' => '2px',
],
'.extbase-debugger-center .extbase-debug-unregistered' => [
'background-color' => 'var(--typo3-debugger-unregistered-bg, #dce1e8)',
],
'.extbase-debugger-center .extbase-debug-scope' => [
'background-color' => 'var(--typo3-debugger-scope-bg, #497AA2)',
],
'.extbase-debugger-center .extbase-debug-ptype' => [
'background-color' => 'var(--typo3-debugger-ptype-bg, #698747)',
],
'.extbase-debugger-center .extbase-debug-visibility' => [
'background-color' => 'var(--typo3-debugger-visibility-bg, #6c0787)',
],
'.extbase-debugger-center .extbase-debug-uninitialized' => [
'background-color' => 'var(--typo3-debugger-uninitializedy-bg, #698747)',
],
'.extbase-debugger-center .extbase-debug-dirty' => [
'background-color' => 'var(--typo3-debugger-dirty-bg, #664d00)',
],
'.extbase-debugger-center .extbase-debug-filtered' => [
'background-color' => 'var(--typo3-debugger-filtered-bg, #664d00)',
],
'.extbase-debugger-center .extbase-debug-string' => [
'color' => 'var(--typo3-debugger-string-color, #ce9178)',
'white-space' => 'normal',
],
'.extbase-debugger-center .extbase-debug-type' => [
'color' => 'var(--typo3-debugger-type-color, #569CD6)',
'padding-right' => '4px',
],
'.extbase-debugger-center .extbase-debug-closure' => [
'color' => 'var(--typo3-debugger-closure-color, #9BA223)',
'white-space' => 'pre',
],
'.extbase-debugger-center .extbase-debug-property' => [
'color' => 'var(--typo3-debugger-property-color, #f1f1f1)',
],
'.extbase-debugger-center .extbase-debug-seeabove' => [
'display' => 'block',
'text-decoration' => 'none',
'font-style' => 'italic',
],
]);
$style = '';
if (!$plainText && self::$stylesheetEchoed === false) {
$style = self::html(
'style',
['nonce' => self::resolveNonceValue(Directive::StyleSrcElem)],
static fn(): string => $css
)();
self::$stylesheetEchoed = true;
}
if ($plainText) {
$output = $title . self::render(
[
PHP_EOL,
self::renderDump($variable, 0, true, $ansiColors),
PHP_EOL,
PHP_EOL,
],
self::escapeConsoleText(...)
);
} else {
$output = self::html('div', ['class' => 'extbase-debugger ' . ($return ? 'extbase-debugger-inline' : 'extbase-debugger-floating')], [
self::html('div', ['class' => 'extbase-debugger-top'], $title),
self::html('div', ['class' => 'extbase-debugger-center'], [
self::html('div', ['class' => 'extbase-debugger-inner'], [
self::renderDump($variable, 0, false, false),
]),
]),
])();
}
self::$blacklistedClassNames = $backupBlacklistedClassNames;
self::$blacklistedPropertyNames = $backupBlacklistedPropertyNames;
if ($return === true) {
return $style . $output;
}
echo $style . $output;
return '';
}
protected static function resolveNonceValue(Directive $directive): string
{
return GeneralUtility::makeInstance(RequestId::class)->nonce->consumeInline($directive);
}
protected static function styled(
string $content,
string $style,
bool $plainText,
bool $ansiColors,
int $spaceBefore = 0,
int $spaceAfter = 0,
): callable|string {
$styleMap = [
'expander' => '',
'string' => '33',
'closure' => '33',
'type' => '36',
'property' => '37',
'ptype' => '42;30',
'visibility' => '42;30',
'dirty' => '43;30',
'scope' => '44;37',
'filtered' => '47;30',
'uninitialized' => '45;37',
];
if (!isset($styleMap[$style])) {
throw new \InvalidArgumentException('Invalid debugger style: ' . $style, 1726659808);
}
if ($plainText) {
if ($style === 'expander') {
return '';
}
return static fn() => [
str_repeat(' ', $spaceBefore),
self::ansiEscapeWrap($content, $styleMap[$style], $plainText, $ansiColors),
str_repeat(' ', $spaceAfter),
];
}
return self::html('span', ['class' => 'extbase-debug-' . $style], $content);
}
protected static function html(string $tagName, array $attributes, string|array|callable|null $content = null): callable
{
if ($tagName === '' || !preg_match('/^[a-zA-Z][a-zA-Z0-9-]*$/', $tagName)) {
throw new \InvalidArgumentException('Invalid tag name', 1726659807);
}
return static fn(): string => implode('', [
'<',
$tagName,
count($attributes) > 0 ? ' ' : '',
// filter null attributes
GeneralUtility::implodeAttributes(array_filter($attributes, static fn(?string $value): bool => $value !== null), true, true),
'>',
...($content === null ? [] : [
self::render($content, self::escapeHtml(...)),
'</',
$tagName,
'>',
]),
]);
}
protected static function escapeHtml(string $text): string
{
return htmlspecialchars($text, ENT_HTML5, 'UTF-8');
}
protected static function escapeConsoleText(string $text): string
{
return preg_replace_callback(
'/[\x00-\x1F\x7F]/u',
static fn(array $matches): string => $matches[0] === PHP_EOL ? PHP_EOL : '\\x' . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT),
$text
) ?? $text;
}
protected static function render(string|callable|array $content, callable $escape): string
{
if (is_string($content)) {
return $escape($content);
}
if ($content instanceof \Closure) {
$content = $content();
if (is_string($content)) {
return $content;
}
}
if (is_array($content)) {
return implode('', array_map(static fn(string|callable|array $content): string => self::render($content, $escape), $content));
}
throw new \InvalidArgumentException('Invalid callable return type: ' . gettype($content), 1726673500);
}
/**
* Converts a CSS tree to a CSS stylesheet string
*
* Example input:
*
* [
* '.my-class' => [
* 'display' => 'block',
* 'color' => 'black',
* ],
* '.other-class' => [
* 'display' => 'flex',
* ],
* ]
*
* Output:
* .my-class{display:block;color:black}
* .other-class{display:flex}
*
* @param array<string, array<string, string>> $cssTree
*/
protected static function cssTreeToString(array $cssTree): string
{
$rules = array_map(
static fn(string $selector): string => sprintf(
'%s{%s}',
$selector,
implode(
';',
array_map(
static fn(string $property): string => sprintf(
'%s:%s',
$property,
$cssTree[$selector][$property]
),
array_keys($cssTree[$selector])
)
)
),
array_keys($cssTree)
);
$stylesheet = implode(PHP_EOL, $rules);
// Optimize away uneeded whitespace
return str_replace(', ', ',', $stylesheet);
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Utility\Exception;
use TYPO3\CMS\Extbase\Exception;
/**
* An Invalid Type Exception
*/
class InvalidTypeException extends Exception {}
+227
View File
@@ -0,0 +1,227 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Utility;
use TYPO3\CMS\Core\Schema\Struct\SelectItem;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Utilities to manage plugins and modules of an extension. Also useful to auto-generate the autoloader registry
* file ext_autoload.php.
*/
class ExtensionUtility
{
public const PLUGIN_TYPE_CONTENT_ELEMENT = 'CType';
/**
* Add auto-generated TypoScript to configure the Extbase Dispatcher.
*
* When adding a frontend plugin you will have to add both an entry to the TCA definition
* of tt_content table AND to the TypoScript template which must initiate the rendering.
* Including the plugin code after "defaultContentRendering" adds the necessary TypoScript
* for calling the appropriate controller and action of your plugin.
* FOR USE IN ext_localconf.php FILES
* Usage: 2
*
* @param string $extensionName The extension name (in UpperCamelCase) or the extension key (in lower_underscore)
* @param string $pluginName must be a unique id for your plugin in UpperCamelCase (the string length of the extension key added to the length of the plugin name should be less than 32!)
* @param array $controllerActions is an array of allowed combinations of controller and action stored in an array (controller name as key and a comma separated list of action names as value, the first controller and its first action is chosen as default)
* @param array $nonCacheableControllerActions is an optional array of controller name and action names which should not be cached (array as defined in $controllerActions)
* @param string|null $pluginType must be omitted or set to \TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT - actually unused in the method, for b/w compatibility only
* @throws \InvalidArgumentException
*/
public static function configurePlugin($extensionName, $pluginName, array $controllerActions, array $nonCacheableControllerActions = [], ?string $pluginType = null): void
{
self::checkPluginNameFormat($pluginName);
self::checkExtensionNameFormat($extensionName);
$pluginType ??= self::PLUGIN_TYPE_CONTENT_ELEMENT;
if ($pluginType !== self::PLUGIN_TYPE_CONTENT_ELEMENT) {
throw new \InvalidArgumentException('Fifth parameter $pluginType has to be omitted or set to "CType"', 1730801526);
}
$extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionName)));
$pluginSignature = strtolower($extensionName . '_' . $pluginName);
$controllerActions = self::actionCommaListToArray($controllerActions);
$nonCacheableControllerActions = self::actionCommaListToArray($nonCacheableControllerActions);
self::registerControllerActions($extensionName, $pluginName, $controllerActions, $nonCacheableControllerActions);
$pluginConfiguration = trim('
tt_content.' . $pluginSignature . ' =< lib.contentElement
tt_content.' . $pluginSignature . ' {
templateName = Generic
20 = EXTBASEPLUGIN
20 {
extensionName = ' . $extensionName . '
pluginName = ' . $pluginName . '
}
}');
ExtensionManagementUtility::addTypoScript($extensionName, 'setup', '
# Setting ' . $extensionName . ' plugin TypoScript
' . $pluginConfiguration, 'defaultContentRendering');
}
/**
* @param array<string, string[]> $controllerActions
* @param array<string, string[]> $nonCacheableControllerActions
* @internal
*/
public static function registerControllerActions(string $extensionName, string $pluginName, array $controllerActions, array $nonCacheableControllerActions): void
{
if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName] ?? false)) {
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName] = [];
}
foreach ($controllerActions as $controllerClassName => $actionsList) {
$controllerAlias = self::resolveControllerAliasFromControllerClassName($controllerClassName);
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName]['controllers'][$controllerClassName] = [
'className' => $controllerClassName,
'alias' => $controllerAlias,
'actions' => $actionsList,
];
if (!empty($nonCacheableControllerActions[$controllerClassName])) {
$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName]['controllers'][$controllerClassName]['nonCacheableActions']
= $nonCacheableControllerActions[$controllerClassName];
}
}
}
/**
* Register an Extbase PlugIn into backend's list of plugins
* FOR USE IN Configuration/TCA/Overrides/tt_content.php
*
* @param string $extensionName The extension name (in UpperCamelCase) or the extension key (in lower_underscore)
* @param string $pluginName must be a unique id for your plugin in UpperCamelCase (the string length of the extension key added to the length of the plugin name should be less than 32!)
* @param string $pluginTitle is a speaking title of the plugin that will be displayed in the drop down menu in the backend
* @param string|null $pluginIcon is an icon identifier or file path prepended with "EXT:", that will be displayed in the drop down menu in the backend (optional)
* @param string $group add this plugin to a plugin group, should be something like "news" or the like, "plugins" as regular
* @param string $pluginDescription additional description
* @param string $flexForm The flex form (data structure) to be used for the plugin. Either a reference to a flex-form XML file (eg. "FILE:EXT:newloginbox/flexform_ds.xml") or the XML directly.
* @throws \InvalidArgumentException
*/
public static function registerPlugin($extensionName, $pluginName, $pluginTitle, $pluginIcon = null, $group = 'plugins', string $pluginDescription = '', string $flexForm = ''): string
{
self::checkPluginNameFormat($pluginName);
self::checkExtensionNameFormat($extensionName);
$extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionName)));
$pluginSignature = strtolower($extensionName) . '_' . strtolower($pluginName);
ExtensionManagementUtility::addPlugin(
new SelectItem(
'select',
// set pluginName as default pluginTitle
$pluginTitle ?: $pluginName,
$pluginSignature,
$pluginIcon ?? 'content-plugin',
$group,
$pluginDescription
),
$flexForm
);
return $pluginSignature;
}
/**
* @internal only used for TYPO3 Core
*/
public static function resolveControllerAliasFromControllerClassName(string $controllerClassName): string
{
// This method has been adjusted for TYPO3 10.3 to mitigate the issue that controller aliases
// could not longer be calculated from controller classes when calling
// \TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin().
//
// The idea for version 11 is to let the user choose a controller alias and to check for its
// uniqueness per plugin. That way, the core does no longer rely on the namespace of
// controller classes to be in a specific format.
//
// todo: Change the way plugins are registered and enforce a controller alias to be set by
// the user to also free the core from guessing a simple alias by looking at the
// class name. This makes it possible to choose controller class names without a
// controller suffix.
$strLen = strlen('Controller');
if (!str_ends_with($controllerClassName, 'Controller')) {
return '';
}
$controllerClassNameWithoutControllerSuffix = substr($controllerClassName, 0, -$strLen);
if (strrpos($controllerClassNameWithoutControllerSuffix, 'Controller\\') === false) {
$positionOfLastSlash = (int)strrpos($controllerClassNameWithoutControllerSuffix, '\\');
$positionOfLastSlash += $positionOfLastSlash === 0 ? 0 : 1;
return substr($controllerClassNameWithoutControllerSuffix, $positionOfLastSlash);
}
$positionOfControllerNamespacePart = (int)strrpos(
$controllerClassNameWithoutControllerSuffix,
'Controller\\'
);
return substr(
$controllerClassNameWithoutControllerSuffix,
$positionOfControllerNamespacePart + $strLen + 1
);
}
/**
* @param array<string, string|string[]> $controllerActions
* @return array<string, string[]>
*/
protected static function actionCommaListToArray(array $controllerActions): array
{
foreach ($controllerActions as $controllerClassName => $actionsList) {
if (is_array($actionsList)) {
continue;
}
$actionsListArray = GeneralUtility::trimExplode(',', (string)$actionsList);
$controllerActions[$controllerClassName] = $actionsListArray;
}
return $controllerActions;
}
/**
* Check a given extension name for validity.
*
* @param string $extensionName The name of the extension
* @throws \InvalidArgumentException
*/
protected static function checkExtensionNameFormat($extensionName)
{
if (empty($extensionName)) {
throw new \InvalidArgumentException('The extension name must not be empty', 1239891990);
}
}
/**
* Check a given plugin name for validity.
*
* @param string $pluginName The name of the plugin
* @throws \InvalidArgumentException
*/
protected static function checkPluginNameFormat($pluginName)
{
if (empty($pluginName)) {
throw new \InvalidArgumentException('The plugin name must not be empty', 1239891988);
}
}
}
+161
View File
@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Utility;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Localization\Locale;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Localization\TranslationDomainMapper;
use TYPO3\CMS\Core\Localization\TranslationDomainResolver;
use TYPO3\CMS\Core\TypoScript\FrontendTypoScript;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Request;
/**
* Localization helper which should be used to fetch localized labels.
*/
class LocalizationUtility
{
/**
* Returns the localized label of the LOCAL_LANG key, $key.
*
* @param string $key The key from the LOCAL_LANG array for which to return the value.
* @param string|null $extensionName The name of the extension or domain name (such as "myextension.messages")
* @param array|null $arguments The arguments for the label. For sprintf-style labels (e.g., "Downloaded %d times"),
* use positional arguments [42]. For ICU MessageFormat labels
* (e.g., "{count, plural, one {# file} other {# files}}"), use named arguments ['count' => 42].
* @param Locale|string|null $languageKey The language key or null for using the current language from the system
* @return string|null The value from LOCAL_LANG or null if no translation was found.
*/
public static function translate(string $key, ?string $extensionName = null, ?array $arguments = null, Locale|string|null $languageKey = null, ?ServerRequestInterface $request = null): ?string
{
if ($key === '') {
// Early return guard: returns null if the key was empty, because the key may be a dynamic value
// (from for example Fluid). Returning null allows null coalescing to a default value when that happens.
return null;
}
$languageFilePath = null;
if (str_starts_with($key, 'LLL:EXT:')) {
$keyParts = explode(':', $key);
unset($keyParts[0]);
$key = array_pop($keyParts);
$languageFilePath = implode(':', $keyParts);
} elseif ($extensionName) {
if (str_contains($extensionName, '.')) {
// We assume this is a valid domain now
$languageFilePath = $extensionName;
$extensionName = self::getExtensionNameFromDomain($languageFilePath);
} else {
$languageFilePath = GeneralUtility::camelCaseToLowerCaseUnderscored($extensionName) . '.messages';
}
} elseif (str_contains($key, ':')) {
[$languageFilePath, $key, $extensionName] = self::extractLanguageInfoFromDomainString($key);
}
if ($languageFilePath === null || $key === '') {
throw new \InvalidArgumentException(
'Parameter $extensionName cannot be empty if a fully-qualified key is not specified.',
1498144052
);
}
$request = $request ?? $GLOBALS['TYPO3_REQUEST'] ?? null;
$locale = self::getLocale($languageKey, $request);
$languageService = GeneralUtility::makeInstance(LanguageServiceFactory::class)->create($locale);
if (!empty($extensionName) && $request instanceof ServerRequestInterface) {
$typoScript = $request->getAttribute('frontend.typoscript');
if ($typoScript instanceof FrontendTypoScript) {
// Loads local-language values by looking for a "locallang.xlf" file in the plugin resources directory and if found includes it.
// Locallang values set in the TypoScript property "_LOCAL_LANG" are merged onto the values found in the "locallang.xlf" file.
$overrideLabels = $languageService->loadTypoScriptLabelsFromExtension($extensionName, $typoScript, self::getPluginName($request));
if ($overrideLabels !== []) {
$languageService->overrideLabels($languageFilePath, $overrideLabels);
}
}
}
try {
return $languageService->translate($key, $languageFilePath, $arguments ?? []);
} catch (\ValueError $e) {
return sprintf('Error: could not translate key "%s" with value "%s" and %d argument(s)!', $key, $e->getMessage(), count($arguments));
}
}
/**
* Resolves the currently active locale.
* Using the Locales factory, as it handles dependencies (e.g. "de-AT" falls back to "de").
*/
protected static function getLocale(Locale|string|null $localeOrLanguageKey, ?ServerRequestInterface $request): Locale
{
if ($localeOrLanguageKey instanceof Locale) {
return $localeOrLanguageKey;
}
$localeFactory = GeneralUtility::makeInstance(Locales::class);
if (is_string($localeOrLanguageKey)) {
return $localeFactory->createLocale($localeOrLanguageKey);
}
return $localeFactory->createLocaleFromRequest($request);
}
/**
* Allow plugin.tx_myextension._LOCAL_LANG and plugin.tx_myextension_myplugin._LOCAL_LANG
*/
protected static function getPluginName(?ServerRequestInterface $request): string
{
if ($request instanceof Request) {
return strtolower($request->getPluginName());
}
return '';
}
private static function getExtensionNameFromDomain(string $possibleDomain): ?string
{
[$extensionName, $filePart] = explode('.', $possibleDomain, 2);
if ($filePart !== 'messages') {
return null;
}
return $extensionName;
}
/**
* @return array<?string>
*/
private static function extractLanguageInfoFromDomainString(string $key): array
{
$languageFilePath = null;
$extensionName = null;
if (str_starts_with($key, 'LLL:')) {
$key = substr($key, 4);
}
[$possibleDomain, $possibleId] = explode(':', $key, 2);
$domainMapper = GeneralUtility::makeInstance(TranslationDomainMapper::class);
$domainResolver = GeneralUtility::makeInstance(TranslationDomainResolver::class);
if ($domainResolver->isValidDomainName($possibleDomain) && $domainMapper->mapDomainToFileName($possibleDomain) !== $possibleDomain) {
$languageFilePath = $possibleDomain;
$key = trim($possibleId);
$extensionName = self::getExtensionNameFromDomain($languageFilePath);
}
return [
$languageFilePath,
$key,
$extensionName,
];
}
}
+152
View File
@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Extbase\Utility;
use TYPO3\CMS\Core\Type\TypeInterface;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Utility\Exception\InvalidTypeException;
/**
* PHP type handling functions
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
class TypeHandlingUtility
{
/**
* A property type parse pattern.
*/
public const PARSE_TYPE_PATTERN = '/^\\\\?(?P<type>integer|int|float|double|boolean|bool|string|DateTimeImmutable|DateTime|Country|[A-Z][a-zA-Z0-9\\\\]+|object|resource|array|ArrayObject|SplObjectStorage|TYPO3\\\\CMS\\\\Extbase\\\\Persistence\\\\ObjectStorage)(?:<\\\\?(?P<elementType>[a-zA-Z0-9\\\\]+)>)?/';
/**
* A type pattern to detect literal types.
*/
public const LITERAL_TYPE_PATTERN = '/^(?:integer|int|float|double|boolean|bool|string)$/';
/**
* @var array
*/
protected static $collectionTypes = ['array', \ArrayObject::class, \SplObjectStorage::class, ObjectStorage::class];
/**
* Returns an array with type information, including element type for
* collection types (array, SplObjectStorage, ...)
*
* @param string $type Type of the property (see PARSE_TYPE_PATTERN)
* @return array An array with information about the type
* @throws \TYPO3\CMS\Extbase\Utility\Exception\InvalidTypeException
*/
public static function parseType(string $type): array
{
$matches = [];
if (preg_match(self::PARSE_TYPE_PATTERN, $type, $matches)) {
$type = self::normalizeType($matches['type']);
$elementType = isset($matches['elementType']) ? self::normalizeType($matches['elementType']) : null;
if ($elementType !== null && !self::isCollectionType($type)) {
throw new InvalidTypeException('Found an invalid element type declaration in %s. Type "' . $type . '" must not have an element type hint (' . $elementType . ').', 1264093642);
}
return [
'type' => $type,
'elementType' => $elementType,
];
}
throw new InvalidTypeException('Found an invalid element type declaration in %s. A type "' . var_export($type, true) . '" does not exist.', 1264093630);
}
/**
* Normalize data types so they match the PHP type names:
* int -> integer
* double -> float
* bool -> boolean
*
* @param string $type Data type to unify
* @return string unified data type
*/
public static function normalizeType(string $type): string
{
switch ($type) {
case 'int':
$type = 'integer';
break;
case 'bool':
$type = 'boolean';
break;
case 'double':
$type = 'float';
break;
}
return $type;
}
/**
* Returns TRUE if the $type is a literal.
*/
public static function isLiteral(string $type): bool
{
return preg_match(self::LITERAL_TYPE_PATTERN, $type) === 1;
}
/**
* Returns TRUE if the $type is a simple type.
*/
public static function isSimpleType(string $type): bool
{
return in_array(self::normalizeType($type), ['array', 'string', 'float', 'integer', 'boolean'], true);
}
/**
* Returns TRUE if the $type is a CMS core type object.
*
* @param string|object $type
*/
public static function isCoreType($type): bool
{
return is_subclass_of($type, TypeInterface::class);
}
/**
* Returns TRUE if the $type is a collection type.
*/
public static function isCollectionType(string $type): bool
{
if (in_array($type, self::$collectionTypes, true)) {
return true;
}
if (class_exists($type) === true || interface_exists($type) === true) {
foreach (self::$collectionTypes as $collectionType) {
if (is_subclass_of($type, $collectionType) === true) {
return true;
}
}
}
return false;
}
/**
* Returns TRUE when the given value can be used in an "in" comparison in a query.
*
* @param mixed $value
*/
public static function isValidTypeForMultiValueComparison($value): bool
{
return is_iterable($value);
}
}