vendor/shopware/core/Framework/Feature.php line 203

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework;
  3. use PHPUnit\Framework\TestCase;
  4. use Shopware\Core\DevOps\Environment\EnvironmentHelper;
  5. use Shopware\Core\Framework\Log\Package;
  6. use Shopware\Core\Framework\Script\Debugging\ScriptTraces;
  7. #[Package('core')]
  8. class Feature
  9. {
  10. public const ALL_MAJOR = 'major';
  11. /**
  12. * @var array<bool>
  13. */
  14. private static array $silent = [];
  15. /**
  16. * @var array<string, array{name?: string, default?: boolean, major?: boolean, description?: string}>
  17. */
  18. private static array $registeredFeatures = [];
  19. public static function normalizeName(string $name): string
  20. {
  21. /*
  22. * Examples:
  23. * - NEXT-1234
  24. * - FEATURE_NEXT_1234
  25. * - SAAS_321
  26. * - v6.5.0.0 => v6_5_0_0
  27. */
  28. return \strtoupper(\str_replace(['.', ':', '-'], '_', $name));
  29. }
  30. /**
  31. * @param array<string> $features
  32. *
  33. * @return mixed|null
  34. */
  35. public static function fake(array $features, \Closure $closure)
  36. {
  37. $before = self::$registeredFeatures;
  38. $serverVarsBackup = $_SERVER;
  39. $result = null;
  40. try {
  41. self::$registeredFeatures = [];
  42. foreach ($_SERVER as $key => $value) {
  43. if (str_starts_with($key, 'v6.') || $key === 'PERFORMANCE_TWEAKS' || str_starts_with($key, 'FEATURE_') || str_starts_with($key, 'V6_')) {
  44. // set to false so that $_ENV is not checked
  45. $_SERVER[$key] = false;
  46. }
  47. }
  48. if ($features) {
  49. foreach ($features as $feature) {
  50. $_SERVER[Feature::normalizeName($feature)] = true;
  51. }
  52. }
  53. $result = $closure();
  54. } finally {
  55. self::$registeredFeatures = $before;
  56. $_SERVER = $serverVarsBackup;
  57. }
  58. return $result;
  59. }
  60. public static function isActive(string $feature): bool
  61. {
  62. $env = EnvironmentHelper::getVariable('APP_ENV', 'prod');
  63. $feature = self::normalizeName($feature);
  64. if (self::$registeredFeatures !== []
  65. && !isset(self::$registeredFeatures[$feature])
  66. && $env !== 'prod'
  67. ) {
  68. trigger_error('Unknown feature "' . $feature . '"', \E_USER_WARNING);
  69. }
  70. $featureAll = EnvironmentHelper::getVariable('FEATURE_ALL', '');
  71. if (self::isTrue((string) $featureAll) && (self::$registeredFeatures === [] || \array_key_exists($feature, self::$registeredFeatures))) {
  72. if ($featureAll === Feature::ALL_MAJOR) {
  73. return true;
  74. }
  75. // return true if it's registered and not a major feature
  76. if (isset(self::$registeredFeatures[$feature]) && (self::$registeredFeatures[$feature]['major'] ?? false) === false) {
  77. return true;
  78. }
  79. }
  80. if (!EnvironmentHelper::hasVariable($feature) && !EnvironmentHelper::hasVariable(\strtolower($feature))) {
  81. $fallback = self::$registeredFeatures[$feature]['default'] ?? false;
  82. return (bool) $fallback;
  83. }
  84. return self::isTrue(trim((string) EnvironmentHelper::getVariable($feature)));
  85. }
  86. public static function ifActive(string $flagName, \Closure $closure): void
  87. {
  88. self::isActive($flagName) && $closure();
  89. }
  90. public static function callSilentIfInactive(string $flagName, \Closure $closure): void
  91. {
  92. $before = isset(self::$silent[$flagName]);
  93. self::$silent[$flagName] = true;
  94. try {
  95. if (!self::isActive($flagName)) {
  96. $closure();
  97. }
  98. } finally {
  99. if (!$before) {
  100. unset(self::$silent[$flagName]);
  101. }
  102. }
  103. }
  104. /**
  105. * @deprecated tag:v6.5.0 - Will be removed, use Feature::isActive instead
  106. *
  107. * @param object $object
  108. * @param mixed[] $arguments
  109. */
  110. public static function ifActiveCall(string $flagName, $object, string $methodName, ...$arguments): void
  111. {
  112. Feature::triggerDeprecationOrThrow(
  113. 'v6.5.0.0',
  114. Feature::deprecatedMethodMessage(__CLASS__, __METHOD__, 'v6.5.0.0', 'Feature::isActive')
  115. );
  116. $closure = function () use ($object, $methodName, $arguments): void {
  117. $object->{$methodName}(...$arguments);
  118. };
  119. self::ifActive($flagName, \Closure::bind($closure, $object, $object));
  120. }
  121. public static function skipTestIfInActive(string $flagName, TestCase $test): void
  122. {
  123. if (self::isActive($flagName)) {
  124. return;
  125. }
  126. $test::markTestSkipped('Skipping feature test for flag "' . $flagName . '"');
  127. }
  128. public static function skipTestIfActive(string $flagName, TestCase $test): void
  129. {
  130. if (!self::isActive($flagName)) {
  131. return;
  132. }
  133. $test::markTestSkipped('Skipping feature test for flag "' . $flagName . '"');
  134. }
  135. /**
  136. * Triggers a silenced deprecation notice.
  137. *
  138. * @param string $sinceVersion The version of the package that introduced the deprecation
  139. * @param string $removeVersion The version of the package when the deprectated code will be removed
  140. * @param string $message The message of the deprecation
  141. * @param mixed ...$args Values to insert in the message using printf() formatting
  142. *
  143. * @deprecated tag:v6.5.0 - will be removed, use `triggerDeprecationOrThrow` instead
  144. */
  145. public static function triggerDeprecated(string $flag, string $sinceVersion, string $removeVersion, string $message, ...$args): void
  146. {
  147. self::triggerDeprecationOrThrow(
  148. 'v6.5.0.0',
  149. self::deprecatedMethodMessage(__CLASS__, __METHOD__, 'v6.5.0.0', 'Feature::triggerDeprecationOrThrow()')
  150. );
  151. $message = 'Deprecated tag:' . $removeVersion . '(flag:' . $flag . '). ' . $message;
  152. if (self::isActive($flag) || !self::has($flag)) {
  153. if (\PHP_SAPI !== 'cli') {
  154. ScriptTraces::addDeprecationNotice(sprintf($message, ...$args));
  155. }
  156. trigger_deprecation('shopware/core', $sinceVersion, $message, $args);
  157. }
  158. }
  159. public static function throwException(string $flag, string $message, bool $state = true): void
  160. {
  161. if (self::isActive($flag) === $state || (self::$registeredFeatures !== [] && !self::has($flag))) {
  162. throw new \RuntimeException($message);
  163. }
  164. if (\PHP_SAPI !== 'cli') {
  165. ScriptTraces::addDeprecationNotice($message);
  166. }
  167. }
  168. public static function triggerDeprecationOrThrow(string $majorFlag, string $message): void
  169. {
  170. if (self::isActive($majorFlag) || (self::$registeredFeatures !== [] && !self::has($majorFlag))) {
  171. throw new \RuntimeException('Tried to access deprecated functionality: ' . $message);
  172. }
  173. if (!isset(self::$silent[$majorFlag]) || !self::$silent[$majorFlag]) {
  174. if (\PHP_SAPI !== 'cli') {
  175. ScriptTraces::addDeprecationNotice($message);
  176. }
  177. trigger_deprecation('shopware/core', '', $message);
  178. }
  179. }
  180. public static function deprecatedMethodMessage(string $class, string $method, string $majorVersion, ?string $replacement = null): string
  181. {
  182. $message = \sprintf(
  183. 'Method "%s::%s()" is deprecated and will be removed in %s.',
  184. $class,
  185. $method,
  186. $majorVersion
  187. );
  188. if ($replacement) {
  189. $message = \sprintf('%s Use "%s" instead.', $message, $replacement);
  190. }
  191. return $message;
  192. }
  193. public static function deprecatedClassMessage(string $class, string $majorVersion, ?string $replacement = null): string
  194. {
  195. $message = \sprintf(
  196. 'Class "%s" is deprecated and will be removed in %s.',
  197. $class,
  198. $majorVersion
  199. );
  200. if ($replacement) {
  201. $message = \sprintf('%s Use "%s" instead.', $message, $replacement);
  202. }
  203. return $message;
  204. }
  205. public static function has(string $flag): bool
  206. {
  207. $flag = self::normalizeName($flag);
  208. return isset(self::$registeredFeatures[$flag]);
  209. }
  210. /**
  211. * @return array<string, bool>
  212. */
  213. public static function getAll(bool $denormalized = true): array
  214. {
  215. $resolvedFlags = [];
  216. foreach (self::$registeredFeatures as $name => $_) {
  217. $active = self::isActive($name);
  218. $resolvedFlags[$name] = $active;
  219. if (!$denormalized) {
  220. continue;
  221. }
  222. $resolvedFlags[self::denormalize($name)] = $active;
  223. }
  224. return $resolvedFlags;
  225. }
  226. /**
  227. * @param array{name?: string, default?: boolean, major?: boolean, description?: string} $metaData
  228. *
  229. * @internal
  230. */
  231. public static function registerFeature(string $name, array $metaData = []): void
  232. {
  233. $name = self::normalizeName($name);
  234. // merge with existing data
  235. /** @var array{name?: string, default?: boolean, major?: boolean, description?: string} $metaData */
  236. $metaData = array_merge(
  237. self::$registeredFeatures[$name] ?? [],
  238. $metaData
  239. );
  240. // set defaults
  241. $metaData['major'] = (bool) ($metaData['major'] ?? false);
  242. $metaData['default'] = (bool) ($metaData['default'] ?? false);
  243. $metaData['description'] = (string) ($metaData['description'] ?? '');
  244. self::$registeredFeatures[$name] = $metaData;
  245. }
  246. /**
  247. * @param array<string, array{name?: string, default?: boolean, major?: boolean, description?: string}>|string[] $registeredFeatures
  248. *
  249. * @internal
  250. */
  251. public static function registerFeatures(iterable $registeredFeatures): void
  252. {
  253. foreach ($registeredFeatures as $flag => $data) {
  254. // old format
  255. if (\is_string($data)) {
  256. $flag = $data;
  257. $data = [];
  258. }
  259. self::registerFeature($flag, $data);
  260. }
  261. }
  262. /**
  263. * @internal
  264. */
  265. public static function resetRegisteredFeatures(): void
  266. {
  267. self::$registeredFeatures = [];
  268. }
  269. /**
  270. * @internal
  271. *
  272. * @return array<string, array{'name'?: string, 'default'?: boolean, 'major'?: boolean, 'description'?: string}>
  273. */
  274. public static function getRegisteredFeatures(): array
  275. {
  276. return self::$registeredFeatures;
  277. }
  278. private static function isTrue(string $value): bool
  279. {
  280. return $value
  281. && $value !== 'false'
  282. && $value !== '0'
  283. && $value !== '';
  284. }
  285. private static function denormalize(string $name): string
  286. {
  287. return \strtolower(\str_replace(['_'], '.', $name));
  288. }
  289. }