diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php index 4c315e735c..0b14604d5a 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -345,6 +345,11 @@ class DeprecationErrorHandler return false; } + // Follow https://no-color.org/ + if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) { + return false; + } + if ('Hyper' === getenv('TERM_PROGRAM')) { return true; } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 1cb14cd7e2..51316ae795 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -48,6 +48,7 @@ use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; use Symfony\Component\DependencyInjection\EnvVarProcessorInterface; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\LogicException; @@ -378,6 +379,8 @@ class FrameworkExtension extends Extension ->addTag('console.command'); $container->registerForAutoconfiguration(ResourceCheckerInterface::class) ->addTag('config_cache.resource_checker'); + $container->registerForAutoconfiguration(EnvVarLoaderInterface::class) + ->addTag('container.env_var_loader'); $container->registerForAutoconfiguration(EnvVarProcessorInterface::class) ->addTag('container.env_var_processor'); $container->registerForAutoconfiguration(ServiceLocator::class) @@ -1348,9 +1351,13 @@ class FrameworkExtension extends Extension } if ($config['decryption_env_var']) { - $container->getDefinition('secrets.decryption_key')->replaceArgument(1, $config['decryption_env_var']); + if (!preg_match('/^(?:\w*+:)*+\w++$/', $config['decryption_env_var'])) { + throw new InvalidArgumentException(sprintf('Invalid value "%s" set as "decryption_env_var": only "word" characters are allowed.', $config['decryption_env_var'])); + } + + $container->getDefinition('secrets.vault')->replaceArgument(1, "%env({$config['decryption_env_var']})%"); } else { - $container->removeDefinition('secrets.decryption_key'); + $container->getDefinition('secrets.vault')->replaceArgument(1, null); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml index d0ba0bcb34..f70243dd84 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml @@ -6,36 +6,13 @@ + %kernel.project_dir%/config/secrets/%kernel.environment% - - - - - - - - - - - - getEnv - - - - base64:default::SYMFONY_DECRYPTION_SECRET + %env(base64:default::SYMFONY_DECRYPTION_SECRET)% %kernel.project_dir%/.env.local - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml index cfe3413cfe..ac406aad07 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml @@ -117,6 +117,12 @@ + + + + + + %kernel.default_locale% diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php index 93d8989502..a64a7449b2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php @@ -54,7 +54,7 @@ class DotenvVault extends AbstractVault { $this->lastMessage = null; $this->validateName($name); - $v = \is_string($_SERVER[$name] ?? null) ? $_SERVER[$name] : ($_ENV[$name] ?? null); + $v = \is_string($_SERVER[$name] ?? null) && 0 !== strpos($name, 'HTTP_') ? $_SERVER[$name] : ($_ENV[$name] ?? null); if (null === $v) { $this->lastMessage = sprintf('Secret "%s" not found in "%s".', $name, $this->getPrettyPath($this->dotenvFile)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/SecretEnvVarProcessor.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/SecretEnvVarProcessor.php deleted file mode 100644 index e3a15febe4..0000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/SecretEnvVarProcessor.php +++ /dev/null @@ -1,59 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Secrets; - -use Symfony\Component\DependencyInjection\EnvVarProcessorInterface; -use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException; - -/** - * @author Tobias Schultze - * @author Nicolas Grekas - * - * @internal - */ -class SecretEnvVarProcessor implements EnvVarProcessorInterface -{ - private $vault; - private $localVault; - - public function __construct(AbstractVault $vault, AbstractVault $localVault = null) - { - $this->vault = $vault; - $this->localVault = $localVault; - } - - /** - * {@inheritdoc} - */ - public static function getProvidedTypes() - { - return [ - 'secret' => 'string', - ]; - } - - /** - * {@inheritdoc} - */ - public function getEnv($prefix, $name, \Closure $getEnv): string - { - if (null !== $this->localVault && null !== ($secret = $this->localVault->reveal($name)) && \array_key_exists($name, $this->vault->list())) { - return $secret; - } - - if (null !== $secret = $this->vault->reveal($name)) { - return $secret; - } - - throw new EnvNotFoundException($this->vault->getLastMessage() ?? sprintf('Secret "%s" not found or decryption key is missing.', $name)); - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php index cb6e9f527f..e6fcab5060 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Secrets; +use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; + /** * @author Tobias Schultze * @author Jérémy Derussé @@ -18,7 +20,7 @@ namespace Symfony\Bundle\FrameworkBundle\Secrets; * * @internal */ -class SodiumVault extends AbstractVault +class SodiumVault extends AbstractVault implements EnvVarLoaderInterface { private $encryptionKey; private $decryptionKey; @@ -56,8 +58,8 @@ class SodiumVault extends AbstractVault // ignore failures to load keys } - if ('' !== $this->decryptionKey && !file_exists($this->pathPrefix.'sodium.encrypt.public')) { - $this->export('sodium.encrypt.public', $this->encryptionKey); + if ('' !== $this->decryptionKey && !file_exists($this->pathPrefix.'encrypt.public.php')) { + $this->export('encrypt.public', $this->encryptionKey); } if (!$override && null !== $this->encryptionKey) { @@ -69,10 +71,10 @@ class SodiumVault extends AbstractVault $this->decryptionKey = sodium_crypto_box_keypair(); $this->encryptionKey = sodium_crypto_box_publickey($this->decryptionKey); - $this->export('sodium.encrypt.public', $this->encryptionKey); - $this->export('sodium.decrypt.private', $this->decryptionKey); + $this->export('encrypt.public', $this->encryptionKey); + $this->export('decrypt.private', $this->decryptionKey); - $this->lastMessage = sprintf('Sodium keys have been generated at "%s*.{public,private}".', $this->getPrettyPath($this->pathPrefix)); + $this->lastMessage = sprintf('Sodium keys have been generated at "%s*.public/private.php".', $this->getPrettyPath($this->pathPrefix)); return true; } @@ -82,12 +84,12 @@ class SodiumVault extends AbstractVault $this->lastMessage = null; $this->validateName($name); $this->loadKeys(); - $this->export($name.'.'.substr_replace(md5($name), '.sodium', -26), sodium_crypto_box_seal($value, $this->encryptionKey ?? sodium_crypto_box_publickey($this->decryptionKey))); + $this->export($name.'.'.substr(md5($name), 0, 6), sodium_crypto_box_seal($value, $this->encryptionKey ?? sodium_crypto_box_publickey($this->decryptionKey))); $list = $this->list(); $list[$name] = null; uksort($list, 'strnatcmp'); - file_put_contents($this->pathPrefix.'sodium.list', sprintf("pathPrefix.'list.php', sprintf("lastMessage = sprintf('Secret "%s" encrypted in "%s"; you can commit it.', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); } @@ -97,7 +99,7 @@ class SodiumVault extends AbstractVault $this->lastMessage = null; $this->validateName($name); - if (!file_exists($file = $this->pathPrefix.$name.'.'.substr_replace(md5($name), '.sodium', -26))) { + if (!file_exists($file = $this->pathPrefix.$name.'.'.substr_replace(md5($name), '.php', -26))) { $this->lastMessage = sprintf('Secret "%s" not found in "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); return null; @@ -131,7 +133,7 @@ class SodiumVault extends AbstractVault $this->lastMessage = null; $this->validateName($name); - if (!file_exists($file = $this->pathPrefix.$name.'.'.substr_replace(md5($name), '.sodium', -26))) { + if (!file_exists($file = $this->pathPrefix.$name.'.'.substr_replace(md5($name), '.php', -26))) { $this->lastMessage = sprintf('Secret "%s" not found in "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); return false; @@ -139,7 +141,7 @@ class SodiumVault extends AbstractVault $list = $this->list(); unset($list[$name]); - file_put_contents($this->pathPrefix.'sodium.list', sprintf("pathPrefix.'list.php', sprintf("lastMessage = sprintf('Secret "%s" removed from "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); @@ -150,7 +152,7 @@ class SodiumVault extends AbstractVault { $this->lastMessage = null; - if (!file_exists($file = $this->pathPrefix.'sodium.list')) { + if (!file_exists($file = $this->pathPrefix.'list.php')) { return []; } @@ -167,6 +169,11 @@ class SodiumVault extends AbstractVault return $secrets; } + public function loadEnvVars(): array + { + return $this->list(true); + } + private function loadKeys(): void { if (!\function_exists('sodium_crypto_box_seal')) { @@ -177,12 +184,12 @@ class SodiumVault extends AbstractVault return; } - if (file_exists($this->pathPrefix.'sodium.decrypt.private')) { - $this->decryptionKey = (string) include $this->pathPrefix.'sodium.decrypt.private'; + if (file_exists($this->pathPrefix.'decrypt.private.php')) { + $this->decryptionKey = (string) include $this->pathPrefix.'decrypt.private.php'; } - if (file_exists($this->pathPrefix.'sodium.encrypt.public')) { - $this->encryptionKey = (string) include $this->pathPrefix.'sodium.encrypt.public'; + if (file_exists($this->pathPrefix.'encrypt.public.php')) { + $this->encryptionKey = (string) include $this->pathPrefix.'encrypt.public.php'; } elseif ('' !== $this->decryptionKey) { $this->encryptionKey = sodium_crypto_box_publickey($this->decryptionKey); } else { @@ -196,7 +203,7 @@ class SodiumVault extends AbstractVault $data = str_replace('%', '\x', rawurlencode($data)); $data = sprintf("pathPrefix.$file, $data, LOCK_EX)) { + if (false === file_put_contents($this->pathPrefix.$file.'.php', $data, LOCK_EX)) { $e = error_get_last(); throw new \ErrorException($e['message'] ?? 'Failed to write secrets data.', 0, $e['type'] ?? E_USER_WARNING); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache_env_var.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache_env_var.php deleted file mode 100644 index 4f819e7204..0000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache_env_var.php +++ /dev/null @@ -1,9 +0,0 @@ -setParameter('env(REDIS_URL)', 'redis://paas.com'); - -$container->loadFromExtension('framework', [ - 'cache' => [ - 'default_redis_provider' => '%env(REDIS_URL)%', - ], -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache_env_var.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache_env_var.xml deleted file mode 100644 index 81c96b3a7f..0000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache_env_var.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - redis://paas.com - - - - - %env(REDIS_URL)% - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache_env_var.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache_env_var.yml deleted file mode 100644 index 1d9ce5f7f0..0000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache_env_var.yml +++ /dev/null @@ -1,6 +0,0 @@ -parameters: - env(REDIS_URL): redis://paas.com - -framework: - cache: - default_redis_provider: "%env(REDIS_URL)%" diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 5694ff6327..72762a7d1d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -34,7 +34,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpClient\ScopingHttpClient; @@ -1202,20 +1202,6 @@ abstract class FrameworkExtensionTest extends TestCase $this->assertSame($redisUrl, $url); } - public function testCacheDefaultRedisProviderWithEnvVar() - { - $container = $this->createContainerFromFile('cache_env_var'); - - $redisUrl = 'redis://paas.com'; - $providerId = '.cache_connection.'.ContainerBuilder::hash($redisUrl); - - $this->assertTrue($container->hasDefinition($providerId)); - - $url = $container->getDefinition($providerId)->getArgument(0); - - $this->assertSame($redisUrl, $url); - } - public function testCachePoolServices() { $container = $this->createContainerFromFile('cache', [], true, false); @@ -1383,7 +1369,7 @@ abstract class FrameworkExtensionTest extends TestCase protected function createContainer(array $data = []) { - return new ContainerBuilder(new ParameterBag(array_merge([ + return new ContainerBuilder(new EnvPlaceholderParameterBag(array_merge([ 'kernel.bundles' => ['FrameworkBundle' => 'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'], 'kernel.bundles_metadata' => ['FrameworkBundle' => ['namespace' => 'Symfony\\Bundle\\FrameworkBundle', 'path' => __DIR__.'/../..']], 'kernel.cache_dir' => __DIR__, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php index 2e25df9024..a9b88b1763 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php @@ -26,19 +26,19 @@ class SodiumVaultTest extends TestCase $vault = new SodiumVault($this->secretsDir); $this->assertTrue($vault->generateKeys()); - $this->assertFileExists($this->secretsDir.'/test.sodium.encrypt.public'); - $this->assertFileExists($this->secretsDir.'/test.sodium.decrypt.private'); + $this->assertFileExists($this->secretsDir.'/test.encrypt.public.php'); + $this->assertFileExists($this->secretsDir.'/test.decrypt.private.php'); - $encKey = file_get_contents($this->secretsDir.'/test.sodium.encrypt.public'); - $decKey = file_get_contents($this->secretsDir.'/test.sodium.decrypt.private'); + $encKey = file_get_contents($this->secretsDir.'/test.encrypt.public.php'); + $decKey = file_get_contents($this->secretsDir.'/test.decrypt.private.php'); $this->assertFalse($vault->generateKeys()); - $this->assertStringEqualsFile($this->secretsDir.'/test.sodium.encrypt.public', $encKey); - $this->assertStringEqualsFile($this->secretsDir.'/test.sodium.decrypt.private', $decKey); + $this->assertStringEqualsFile($this->secretsDir.'/test.encrypt.public.php', $encKey); + $this->assertStringEqualsFile($this->secretsDir.'/test.decrypt.private.php', $decKey); $this->assertTrue($vault->generateKeys(true)); - $this->assertStringNotEqualsFile($this->secretsDir.'/test.sodium.encrypt.public', $encKey); - $this->assertStringNotEqualsFile($this->secretsDir.'/test.sodium.decrypt.private', $decKey); + $this->assertStringNotEqualsFile($this->secretsDir.'/test.encrypt.public.php', $encKey); + $this->assertStringNotEqualsFile($this->secretsDir.'/test.decrypt.private.php', $decKey); } public function testEncryptAndDecrypt() diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 0929c87488..3273efbdea 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -29,6 +29,7 @@ CHANGELOG * deprecated returning `null` from `Command::execute()`, return `0` instead * Deprecated the `Application::renderException()` and `Application::doRenderException()` methods, use `renderThrowable()` and `doRenderThrowable()` instead. + * added support for the `NO_COLOR` env var (https://no-color.org/) 4.3.0 ----- diff --git a/src/Symfony/Component/Console/Output/StreamOutput.php b/src/Symfony/Component/Console/Output/StreamOutput.php index 6ffd9d1279..0b24052e9f 100644 --- a/src/Symfony/Component/Console/Output/StreamOutput.php +++ b/src/Symfony/Component/Console/Output/StreamOutput.php @@ -97,6 +97,11 @@ class StreamOutput extends Output */ protected function hasColorSupport() { + // Follow https://no-color.org/ + if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) { + return false; + } + if ('Hyper' === getenv('TERM_PROGRAM')) { return true; } diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index a59347108f..b043220043 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -26,7 +26,6 @@ CHANGELOG * made singly-implemented interfaces detection be scoped by file * added ability to define a static priority method for tagged service * added support for improved syntax to define method calls in Yaml - * added `LazyString` for lazy computation of string values injected into services * made the `%env(base64:...)%` processor able to decode base64url * added ability to choose behavior of decorations on non existent decorated services diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php index ea4a05cfd9..2147d53f12 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php @@ -188,7 +188,7 @@ final class CheckTypeDeclarationsPass extends AbstractRecursivePass $checkFunction = sprintf('is_%s', $parameter->getType()->getName()); if (!$parameter->getType()->isBuiltin() || !$checkFunction($value)) { - throw new InvalidParameterTypeException($this->currentId, \gettype($value), $parameter); + throw new InvalidParameterTypeException($this->currentId, \is_object($value) ? \get_class($value) : \gettype($value), $parameter); } } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php index 099ed9c342..5985451b34 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php @@ -52,6 +52,8 @@ final class ServiceLocatorTagPass extends AbstractRecursivePass throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set.', $this->currentId)); } + $i = 0; + foreach ($arguments[0] as $k => $v) { if ($v instanceof ServiceClosureArgument) { continue; @@ -60,10 +62,13 @@ final class ServiceLocatorTagPass extends AbstractRecursivePass throw new InvalidArgumentException(sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set, "%s" found for key "%s".', $this->currentId, \is_object($v) ? \get_class($v) : \gettype($v), $k)); } - if (\is_int($k)) { + if ($i === $k) { unset($arguments[0][$k]); $k = (string) $v; + ++$i; + } elseif (\is_int($k)) { + $i = null; } $arguments[0][$k] = new ServiceClosureArgument($v); } diff --git a/src/Symfony/Component/DependencyInjection/EnvVarLoaderInterface.php b/src/Symfony/Component/DependencyInjection/EnvVarLoaderInterface.php new file mode 100644 index 0000000000..0c547f8a5f --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/EnvVarLoaderInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection; + +/** + * EnvVarLoaderInterface objects return key/value pairs that are added to the list of available env vars. + * + * @author Nicolas Grekas + */ +interface EnvVarLoaderInterface +{ + /** + * @return string[] Key/value pairs that can be accessed using the regular "%env()%" syntax + */ + public function loadEnvVars(): array; +} diff --git a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php index ff528a90b1..69c62ad870 100644 --- a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php +++ b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection; use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException; +use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; /** @@ -20,10 +21,17 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; class EnvVarProcessor implements EnvVarProcessorInterface { private $container; + private $loaders; + private $loadedVars = []; - public function __construct(ContainerInterface $container) + /** + * @param EnvVarLoaderInterface[] $loaders + */ + public function __construct(ContainerInterface $container, \Traversable $loaders = null) { $this->container = $container; + $this->loaders = new \IteratorIterator($loaders ?? new \ArrayIterator()); + $this->loaders = $this->loaders->getInnerIterator(); } /** @@ -127,12 +135,31 @@ class EnvVarProcessor implements EnvVarProcessorInterface } elseif (isset($_SERVER[$name]) && 0 !== strpos($name, 'HTTP_')) { $env = $_SERVER[$name]; } elseif (false === ($env = getenv($name)) || null === $env) { // null is a possible value because of thread safety issues - if (!$this->container->hasParameter("env($name)")) { - throw new EnvNotFoundException(sprintf('Environment variable not found: "%s".', $name)); + foreach ($this->loadedVars as $vars) { + if (false !== $env = ($vars[$name] ?? false)) { + break; + } } - if (null === $env = $this->container->getParameter("env($name)")) { - return null; + try { + while ((false === $env || null === $env) && $this->loaders->valid()) { + $loader = $this->loaders->current(); + $this->loaders->next(); + $this->loadedVars[] = $vars = $loader->loadEnvVars(); + $env = $vars[$name] ?? false; + } + } catch (ParameterCircularReferenceException $e) { + // skip loaders that need an env var that is not defined + } + + if (false === $env || null === $env) { + if (!$this->container->hasParameter("env($name)")) { + throw new EnvNotFoundException(sprintf('Environment variable not found: "%s".', $name)); + } + + if (null === $env = $this->container->getParameter("env($name)")) { + return null; + } } } diff --git a/src/Symfony/Component/DependencyInjection/LazyString.php b/src/Symfony/Component/DependencyInjection/LazyString.php deleted file mode 100644 index 6a03c376b8..0000000000 --- a/src/Symfony/Component/DependencyInjection/LazyString.php +++ /dev/null @@ -1,112 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection; - -/** - * A string whose value is computed lazily by a callback. - * - * @author Nicolas Grekas - */ -class LazyString -{ - private $value; - - /** - * @param callable $callback A callable or a [Closure, method] lazy-callable - * - * @return static - */ - public static function fromCallable($callback, ...$arguments): self - { - if (!\is_callable($callback) && !(\is_array($callback) && isset($callback[0]) && $callback[0] instanceof \Closure && 2 >= \count($callback))) { - throw new \TypeError(sprintf('Argument 1 passed to %s() must be a callable or a [Closure, method] lazy-callable, %s given.', __METHOD__, \gettype($callback))); - } - - $lazyString = new static(); - $lazyString->value = static function () use (&$callback, &$arguments, &$value): string { - if (null !== $arguments) { - if (!\is_callable($callback)) { - $callback[0] = $callback[0](); - $callback[1] = $callback[1] ?? '__invoke'; - } - $value = $callback(...$arguments); - $callback = self::getPrettyName($callback); - $arguments = null; - } - - return $value ?? ''; - }; - - return $lazyString; - } - - public function __toString() - { - if (\is_string($this->value)) { - return $this->value; - } - - try { - return $this->value = ($this->value)(); - } catch (\Throwable $e) { - if (\TypeError::class === \get_class($e) && __FILE__ === $e->getFile()) { - $type = explode(', ', $e->getMessage()); - $type = substr(array_pop($type), 0, -\strlen(' returned')); - $r = new \ReflectionFunction($this->value); - $callback = $r->getStaticVariables()['callback']; - - $e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type)); - } - - if (\PHP_VERSION_ID < 70400) { - // leverage the ErrorHandler component with graceful fallback when it's not available - return trigger_error($e, E_USER_ERROR); - } - - throw $e; - } - } - - private function __construct() - { - } - - private static function getPrettyName(callable $callback): string - { - if (\is_string($callback)) { - return $callback; - } - - if (\is_array($callback)) { - $class = \is_object($callback[0]) ? \get_class($callback[0]) : $callback[0]; - $method = $callback[1]; - } elseif ($callback instanceof \Closure) { - $r = new \ReflectionFunction($callback); - - if (false !== strpos($r->name, '{closure}') || !$class = $r->getClosureScopeClass()) { - return $r->name; - } - - $class = $class->name; - $method = $r->name; - } else { - $class = \get_class($callback); - $method = '__invoke'; - } - - if (isset($class[15]) && "\0" === $class[15] && 0 === strpos($class, "class@anonymous\x00")) { - $class = get_parent_class($class).'@anonymous'; - } - - return $class.'::'.$method; - } -} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php index 1de02d2577..66af69b543 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php @@ -114,6 +114,7 @@ class ServiceLocatorTagPassTest extends TestCase ->setArguments([[ 'bar' => new Reference('baz'), new Reference('bar'), + 16 => new Reference('baz'), ]]) ->addTag('container.service_locator') ; @@ -124,6 +125,7 @@ class ServiceLocatorTagPassTest extends TestCase $locator = $container->get('foo'); $this->assertSame(TestDefinition1::class, \get_class($locator('bar'))); + $this->assertSame(TestDefinition2::class, \get_class($locator(16))); } public function testBindingsAreCopied() diff --git a/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php b/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php index 12d852f453..9970eb474f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php @@ -5,6 +5,7 @@ namespace Symfony\Component\DependencyInjection\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; use Symfony\Component\DependencyInjection\EnvVarProcessor; class EnvVarProcessorTest extends TestCase @@ -517,4 +518,39 @@ CSV; [null, null], ]; } + + public function testEnvLoader() + { + $loaders = function () { + yield new class() implements EnvVarLoaderInterface { + public function loadEnvVars(): array + { + return [ + 'FOO_ENV_LOADER' => '123', + ]; + } + }; + + yield new class() implements EnvVarLoaderInterface { + public function loadEnvVars(): array + { + return [ + 'FOO_ENV_LOADER' => '234', + 'BAR_ENV_LOADER' => '456', + ]; + } + }; + }; + + $processor = new EnvVarProcessor(new Container(), $loaders()); + + $result = $processor->getEnv('string', 'FOO_ENV_LOADER', function () {}); + $this->assertSame('123', $result); + + $result = $processor->getEnv('string', 'BAR_ENV_LOADER', function () {}); + $this->assertSame('456', $result); + + $result = $processor->getEnv('string', 'FOO_ENV_LOADER', function () {}); + $this->assertSame('123', $result); // check twice + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/LazyStringTest.php b/src/Symfony/Component/DependencyInjection/Tests/LazyStringTest.php deleted file mode 100644 index 38899bb369..0000000000 --- a/src/Symfony/Component/DependencyInjection/Tests/LazyStringTest.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\DependencyInjection\Tests; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\LazyString; -use Symfony\Component\ErrorHandler\ErrorHandler; - -class LazyStringTest extends TestCase -{ - public function testLazyString() - { - $count = 0; - $s = LazyString::fromCallable(function () use (&$count) { - return ++$count; - }); - - $this->assertSame(0, $count); - $this->assertSame('1', (string) $s); - $this->assertSame(1, $count); - } - - public function testLazyCallable() - { - $count = 0; - $s = LazyString::fromCallable([function () use (&$count) { - return new class($count) { - private $count; - - public function __construct(int &$count) - { - $this->count = &$count; - } - - public function __invoke() - { - return ++$this->count; - } - }; - }]); - - $this->assertSame(0, $count); - $this->assertSame('1', (string) $s); - $this->assertSame(1, $count); - $this->assertSame('1', (string) $s); // ensure the value is memoized - $this->assertSame(1, $count); - } - - /** - * @runInSeparateProcess - */ - public function testReturnTypeError() - { - ErrorHandler::register(); - - $s = LazyString::fromCallable(function () { return []; }); - - $this->expectException(\TypeError::class); - $this->expectExceptionMessage('Return value of '.__NAMESPACE__.'\{closure}() passed to '.LazyString::class.'::fromCallable() must be of the type string, array returned.'); - - (string) $s; - } -} diff --git a/src/Symfony/Component/DependencyInjection/composer.json b/src/Symfony/Component/DependencyInjection/composer.json index 8c7cef09c8..b7fffed9b0 100644 --- a/src/Symfony/Component/DependencyInjection/composer.json +++ b/src/Symfony/Component/DependencyInjection/composer.json @@ -23,7 +23,6 @@ "require-dev": { "symfony/yaml": "^4.4|^5.0", "symfony/config": "^5.0", - "symfony/error-handler": "^4.4|^5.0", "symfony/expression-language": "^4.4|^5.0" }, "suggest": { diff --git a/src/Symfony/Component/VarDumper/CHANGELOG.md b/src/Symfony/Component/VarDumper/CHANGELOG.md index a4eb962fd3..94b1c17d1d 100644 --- a/src/Symfony/Component/VarDumper/CHANGELOG.md +++ b/src/Symfony/Component/VarDumper/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * added the stamps of a message after it is dispatched in `TraceableMessageBus` and `MessengerDataCollector` collected data * added `UuidCaster` * made all casters final + * added support for the `NO_COLOR` env var (https://no-color.org/) 4.3.0 ----- diff --git a/src/Symfony/Component/VarDumper/Dumper/CliDumper.php b/src/Symfony/Component/VarDumper/Dumper/CliDumper.php index 42930a3403..326ce1d863 100644 --- a/src/Symfony/Component/VarDumper/Dumper/CliDumper.php +++ b/src/Symfony/Component/VarDumper/Dumper/CliDumper.php @@ -578,6 +578,11 @@ class CliDumper extends AbstractDumper return false; } + // Follow https://no-color.org/ + if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) { + return false; + } + if ('Hyper' === getenv('TERM_PROGRAM')) { return true; } diff --git a/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php b/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php index 5951f5f800..3ef65b315a 100644 --- a/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php +++ b/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php @@ -61,7 +61,7 @@ final class MethodMarkingStore implements MarkingStoreInterface } if ($this->singleState) { - $marking = [$marking => 1]; + $marking = [(string) $marking => 1]; } return new Marking($marking); diff --git a/src/Symfony/Component/Workflow/Tests/MarkingStore/MethodMarkingStoreTest.php b/src/Symfony/Component/Workflow/Tests/MarkingStore/MethodMarkingStoreTest.php index 7b5c7ffa91..7393faa825 100644 --- a/src/Symfony/Component/Workflow/Tests/MarkingStore/MethodMarkingStoreTest.php +++ b/src/Symfony/Component/Workflow/Tests/MarkingStore/MethodMarkingStoreTest.php @@ -52,4 +52,35 @@ class MethodMarkingStoreTest extends TestCase $this->assertEquals($marking, $marking2); } + + public function testGetMarkingWithValueObject() + { + $subject = new Subject($this->createValueObject('first_place')); + + $markingStore = new MethodMarkingStore(true); + + $marking = $markingStore->getMarking($subject); + + $this->assertInstanceOf(Marking::class, $marking); + $this->assertCount(1, $marking->getPlaces()); + $this->assertSame('first_place', (string) $subject->getMarking()); + } + + private function createValueObject(string $markingValue) + { + return new class($markingValue) { + /** @var string */ + private $markingValue; + + public function __construct(string $markingValue) + { + $this->markingValue = $markingValue; + } + + public function __toString() + { + return $this->markingValue; + } + }; + } }