<?php declare(strict_types=1);namespace Shopware\Core\Framework;use PHPUnit\Framework\TestCase;use Shopware\Core\DevOps\Environment\EnvironmentHelper;use Shopware\Core\Framework\Log\Package;use Shopware\Core\Framework\Script\Debugging\ScriptTraces;#[Package('core')]class Feature{ public const ALL_MAJOR = 'major'; /** * @var array<bool> */ private static array $silent = []; /** * @var array<string, array{name?: string, default?: boolean, major?: boolean, description?: string}> */ private static array $registeredFeatures = []; public static function normalizeName(string $name): string { /* * Examples: * - NEXT-1234 * - FEATURE_NEXT_1234 * - SAAS_321 * - v6.5.0.0 => v6_5_0_0 */ return \strtoupper(\str_replace(['.', ':', '-'], '_', $name)); } /** * @param array<string> $features * * @return mixed|null */ public static function fake(array $features, \Closure $closure) { $before = self::$registeredFeatures; $serverVarsBackup = $_SERVER; $result = null; try { self::$registeredFeatures = []; foreach ($_SERVER as $key => $value) { if (str_starts_with($key, 'v6.') || $key === 'PERFORMANCE_TWEAKS' || str_starts_with($key, 'FEATURE_') || str_starts_with($key, 'V6_')) { // set to false so that $_ENV is not checked $_SERVER[$key] = false; } } if ($features) { foreach ($features as $feature) { $_SERVER[Feature::normalizeName($feature)] = true; } } $result = $closure(); } finally { self::$registeredFeatures = $before; $_SERVER = $serverVarsBackup; } return $result; } public static function isActive(string $feature): bool { $env = EnvironmentHelper::getVariable('APP_ENV', 'prod'); $feature = self::normalizeName($feature); if (self::$registeredFeatures !== [] && !isset(self::$registeredFeatures[$feature]) && $env !== 'prod' ) { trigger_error('Unknown feature "' . $feature . '"', \E_USER_WARNING); } $featureAll = EnvironmentHelper::getVariable('FEATURE_ALL', ''); if (self::isTrue((string) $featureAll) && (self::$registeredFeatures === [] || \array_key_exists($feature, self::$registeredFeatures))) { if ($featureAll === Feature::ALL_MAJOR) { return true; } // return true if it's registered and not a major feature if (isset(self::$registeredFeatures[$feature]) && (self::$registeredFeatures[$feature]['major'] ?? false) === false) { return true; } } if (!EnvironmentHelper::hasVariable($feature) && !EnvironmentHelper::hasVariable(\strtolower($feature))) { $fallback = self::$registeredFeatures[$feature]['default'] ?? false; return (bool) $fallback; } return self::isTrue(trim((string) EnvironmentHelper::getVariable($feature))); } public static function ifActive(string $flagName, \Closure $closure): void { self::isActive($flagName) && $closure(); } public static function callSilentIfInactive(string $flagName, \Closure $closure): void { $before = isset(self::$silent[$flagName]); self::$silent[$flagName] = true; try { if (!self::isActive($flagName)) { $closure(); } } finally { if (!$before) { unset(self::$silent[$flagName]); } } } /** * @deprecated tag:v6.5.0 - Will be removed, use Feature::isActive instead * * @param object $object * @param mixed[] $arguments */ public static function ifActiveCall(string $flagName, $object, string $methodName, ...$arguments): void { Feature::triggerDeprecationOrThrow( 'v6.5.0.0', Feature::deprecatedMethodMessage(__CLASS__, __METHOD__, 'v6.5.0.0', 'Feature::isActive') ); $closure = function () use ($object, $methodName, $arguments): void { $object->{$methodName}(...$arguments); }; self::ifActive($flagName, \Closure::bind($closure, $object, $object)); } public static function skipTestIfInActive(string $flagName, TestCase $test): void { if (self::isActive($flagName)) { return; } $test::markTestSkipped('Skipping feature test for flag "' . $flagName . '"'); } public static function skipTestIfActive(string $flagName, TestCase $test): void { if (!self::isActive($flagName)) { return; } $test::markTestSkipped('Skipping feature test for flag "' . $flagName . '"'); } /** * Triggers a silenced deprecation notice. * * @param string $sinceVersion The version of the package that introduced the deprecation * @param string $removeVersion The version of the package when the deprectated code will be removed * @param string $message The message of the deprecation * @param mixed ...$args Values to insert in the message using printf() formatting * * @deprecated tag:v6.5.0 - will be removed, use `triggerDeprecationOrThrow` instead */ public static function triggerDeprecated(string $flag, string $sinceVersion, string $removeVersion, string $message, ...$args): void { self::triggerDeprecationOrThrow( 'v6.5.0.0', self::deprecatedMethodMessage(__CLASS__, __METHOD__, 'v6.5.0.0', 'Feature::triggerDeprecationOrThrow()') ); $message = 'Deprecated tag:' . $removeVersion . '(flag:' . $flag . '). ' . $message; if (self::isActive($flag) || !self::has($flag)) { if (\PHP_SAPI !== 'cli') { ScriptTraces::addDeprecationNotice(sprintf($message, ...$args)); } trigger_deprecation('shopware/core', $sinceVersion, $message, $args); } } public static function throwException(string $flag, string $message, bool $state = true): void { if (self::isActive($flag) === $state || (self::$registeredFeatures !== [] && !self::has($flag))) { throw new \RuntimeException($message); } if (\PHP_SAPI !== 'cli') { ScriptTraces::addDeprecationNotice($message); } } public static function triggerDeprecationOrThrow(string $majorFlag, string $message): void { if (self::isActive($majorFlag) || (self::$registeredFeatures !== [] && !self::has($majorFlag))) { throw new \RuntimeException('Tried to access deprecated functionality: ' . $message); } if (!isset(self::$silent[$majorFlag]) || !self::$silent[$majorFlag]) { if (\PHP_SAPI !== 'cli') { ScriptTraces::addDeprecationNotice($message); } trigger_deprecation('shopware/core', '', $message); } } public static function deprecatedMethodMessage(string $class, string $method, string $majorVersion, ?string $replacement = null): string { $message = \sprintf( 'Method "%s::%s()" is deprecated and will be removed in %s.', $class, $method, $majorVersion ); if ($replacement) { $message = \sprintf('%s Use "%s" instead.', $message, $replacement); } return $message; } public static function deprecatedClassMessage(string $class, string $majorVersion, ?string $replacement = null): string { $message = \sprintf( 'Class "%s" is deprecated and will be removed in %s.', $class, $majorVersion ); if ($replacement) { $message = \sprintf('%s Use "%s" instead.', $message, $replacement); } return $message; } public static function has(string $flag): bool { $flag = self::normalizeName($flag); return isset(self::$registeredFeatures[$flag]); } /** * @return array<string, bool> */ public static function getAll(bool $denormalized = true): array { $resolvedFlags = []; foreach (self::$registeredFeatures as $name => $_) { $active = self::isActive($name); $resolvedFlags[$name] = $active; if (!$denormalized) { continue; } $resolvedFlags[self::denormalize($name)] = $active; } return $resolvedFlags; } /** * @param array{name?: string, default?: boolean, major?: boolean, description?: string} $metaData * * @internal */ public static function registerFeature(string $name, array $metaData = []): void { $name = self::normalizeName($name); // merge with existing data /** @var array{name?: string, default?: boolean, major?: boolean, description?: string} $metaData */ $metaData = array_merge( self::$registeredFeatures[$name] ?? [], $metaData ); // set defaults $metaData['major'] = (bool) ($metaData['major'] ?? false); $metaData['default'] = (bool) ($metaData['default'] ?? false); $metaData['description'] = (string) ($metaData['description'] ?? ''); self::$registeredFeatures[$name] = $metaData; } /** * @param array<string, array{name?: string, default?: boolean, major?: boolean, description?: string}>|string[] $registeredFeatures * * @internal */ public static function registerFeatures(iterable $registeredFeatures): void { foreach ($registeredFeatures as $flag => $data) { // old format if (\is_string($data)) { $flag = $data; $data = []; } self::registerFeature($flag, $data); } } /** * @internal */ public static function resetRegisteredFeatures(): void { self::$registeredFeatures = []; } /** * @internal * * @return array<string, array{'name'?: string, 'default'?: boolean, 'major'?: boolean, 'description'?: string}> */ public static function getRegisteredFeatures(): array { return self::$registeredFeatures; } private static function isTrue(string $value): bool { return $value && $value !== 'false' && $value !== '0' && $value !== ''; } private static function denormalize(string $name): string { return \strtolower(\str_replace(['_'], '.', $name)); }}