feature #40140 [DependencyInjection] Add ContainerBuilder::willBeAvailable() to help with conditional configuration (nicolas-grekas)

This PR was merged into the 5.3-dev branch.

Discussion
----------

[DependencyInjection] Add ContainerBuilder::willBeAvailable() to help with conditional configuration

| Q             | A
| ------------- | ---
| Branch?       | 5.x
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #40136, fix #39356
| License       | MIT
| Doc PR        | no need to

Leverages https://github.com/composer/composer/pull/9682 to ignore dev-packages when configuring the container.

Commits
-------

47c471e2c4 [DependencyInjection] Add ContainerBuilder::willBeAvailable() to help with conditional configuration
This commit is contained in:
Nicolas Grekas 2021-02-25 17:13:15 +01:00
commit 1849b571b5
12 changed files with 180 additions and 121 deletions

View File

@ -13,7 +13,6 @@ namespace Symfony\Bundle\DebugBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
/**
* DebugExtension configuration structure.
@ -51,21 +50,13 @@ class Configuration implements ConfigurationInterface
->example('php://stderr, or tcp://%env(VAR_DUMPER_SERVER)% when using the "server:dump" command')
->defaultNull()
->end()
->end()
;
if (method_exists(HtmlDumper::class, 'setTheme')) {
$rootNode
->children()
->enumNode('theme')
->info('Changes the color of the dump() output when rendered directly on the templating. "dark" (default) or "light"')
->example('dark')
->values(['dark', 'light'])
->defaultValue('dark')
->end()
->enumNode('theme')
->info('Changes the color of the dump() output when rendered directly on the templating. "dark" (default) or "light"')
->example('dark')
->values(['dark', 'light'])
->defaultValue('dark')
->end()
;
}
return $treeBuilder;
}

View File

@ -21,6 +21,7 @@ use Symfony\Component\Config\Definition\Builder\NodeBuilder;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpClient\HttpClient;
@ -30,6 +31,7 @@ use Symfony\Component\Lock\Store\SemaphoreStore;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Notifier\Notifier;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
use Symfony\Component\Serializer\Serializer;
@ -108,8 +110,19 @@ class Configuration implements ConfigurationInterface
->end()
;
$willBeAvailable = static function (string $package, string $class, string $parentPackage = null) {
$parentPackages = (array) $parentPackage;
$parentPackages[] = 'symfony/framework-bundle';
return ContainerBuilder::willBeAvailable($package, $class, $parentPackages);
};
$enableIfStandalone = static function (string $package, string $class) use ($willBeAvailable) {
return !class_exists(FullStack::class) && $willBeAvailable($package, $class) ? 'canBeDisabled' : 'canBeEnabled';
};
$this->addCsrfSection($rootNode);
$this->addFormSection($rootNode);
$this->addFormSection($rootNode, $enableIfStandalone);
$this->addHttpCacheSection($rootNode);
$this->addEsiSection($rootNode);
$this->addSsiSection($rootNode);
@ -119,25 +132,25 @@ class Configuration implements ConfigurationInterface
$this->addRouterSection($rootNode);
$this->addSessionSection($rootNode);
$this->addRequestSection($rootNode);
$this->addAssetsSection($rootNode);
$this->addTranslatorSection($rootNode);
$this->addValidationSection($rootNode);
$this->addAnnotationsSection($rootNode);
$this->addSerializerSection($rootNode);
$this->addPropertyAccessSection($rootNode);
$this->addPropertyInfoSection($rootNode);
$this->addCacheSection($rootNode);
$this->addAssetsSection($rootNode, $enableIfStandalone);
$this->addTranslatorSection($rootNode, $enableIfStandalone);
$this->addValidationSection($rootNode, $enableIfStandalone, $willBeAvailable);
$this->addAnnotationsSection($rootNode, $willBeAvailable);
$this->addSerializerSection($rootNode, $enableIfStandalone, $willBeAvailable);
$this->addPropertyAccessSection($rootNode, $willBeAvailable);
$this->addPropertyInfoSection($rootNode, $enableIfStandalone);
$this->addCacheSection($rootNode, $willBeAvailable);
$this->addPhpErrorsSection($rootNode);
$this->addWebLinkSection($rootNode);
$this->addLockSection($rootNode);
$this->addMessengerSection($rootNode);
$this->addWebLinkSection($rootNode, $enableIfStandalone);
$this->addLockSection($rootNode, $enableIfStandalone);
$this->addMessengerSection($rootNode, $enableIfStandalone);
$this->addRobotsIndexSection($rootNode);
$this->addHttpClientSection($rootNode);
$this->addMailerSection($rootNode);
$this->addHttpClientSection($rootNode, $enableIfStandalone);
$this->addMailerSection($rootNode, $enableIfStandalone);
$this->addSecretsSection($rootNode);
$this->addNotifierSection($rootNode);
$this->addRateLimiterSection($rootNode);
$this->addUidSection($rootNode);
$this->addNotifierSection($rootNode, $enableIfStandalone);
$this->addRateLimiterSection($rootNode, $enableIfStandalone);
$this->addUidSection($rootNode, $enableIfStandalone);
return $treeBuilder;
}
@ -176,13 +189,13 @@ class Configuration implements ConfigurationInterface
;
}
private function addFormSection(ArrayNodeDefinition $rootNode)
private function addFormSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
->children()
->arrayNode('form')
->info('form configuration')
->{!class_exists(FullStack::class) && class_exists(Form::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/form', Form::class)}()
->children()
->arrayNode('csrf_protection')
->treatFalseLike(['enabled' => false])
@ -675,13 +688,13 @@ class Configuration implements ConfigurationInterface
;
}
private function addAssetsSection(ArrayNodeDefinition $rootNode)
private function addAssetsSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
->children()
->arrayNode('assets')
->info('assets configuration')
->{!class_exists(FullStack::class) && class_exists(Package::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/asset', Package::class)}()
->fixXmlConfig('base_url')
->children()
->scalarNode('version_strategy')->defaultNull()->end()
@ -763,13 +776,13 @@ class Configuration implements ConfigurationInterface
;
}
private function addTranslatorSection(ArrayNodeDefinition $rootNode)
private function addTranslatorSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
->children()
->arrayNode('translator')
->info('translator configuration')
->{!class_exists(FullStack::class) && class_exists(Translator::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/translation', Translator::class)}()
->fixXmlConfig('fallback')
->fixXmlConfig('path')
->fixXmlConfig('enabled_locale')
@ -816,16 +829,16 @@ class Configuration implements ConfigurationInterface
;
}
private function addValidationSection(ArrayNodeDefinition $rootNode)
private function addValidationSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone, callable $willBeAvailable)
{
$rootNode
->children()
->arrayNode('validation')
->info('validation configuration')
->{!class_exists(FullStack::class) && class_exists(Validation::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/validator', Validation::class)}()
->children()
->scalarNode('cache')->end()
->booleanNode('enable_annotations')->{!class_exists(FullStack::class) && class_exists(Annotation::class) ? 'defaultTrue' : 'defaultFalse'}()->end()
->booleanNode('enable_annotations')->{!class_exists(FullStack::class) && $willBeAvailable('doctrine/annotations', Annotation::class, 'symfony/validator') ? 'defaultTrue' : 'defaultFalse'}()->end()
->arrayNode('static_method')
->defaultValue(['loadValidatorMetadata'])
->prototype('scalar')->end()
@ -906,15 +919,15 @@ class Configuration implements ConfigurationInterface
;
}
private function addAnnotationsSection(ArrayNodeDefinition $rootNode)
private function addAnnotationsSection(ArrayNodeDefinition $rootNode, callable $willBeAvailable)
{
$rootNode
->children()
->arrayNode('annotations')
->info('annotation configuration')
->{class_exists(Annotation::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$willBeAvailable('doctrine/annotations', Annotation::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->children()
->scalarNode('cache')->defaultValue(interface_exists(Cache::class) ? 'php_array' : 'none')->end()
->scalarNode('cache')->defaultValue($willBeAvailable('doctrine/cache', Cache::class, 'doctrine/annotation') ? 'php_array' : 'none')->end()
->scalarNode('file_cache_dir')->defaultValue('%kernel.cache_dir%/annotations')->end()
->booleanNode('debug')->defaultValue($this->debug)->end()
->end()
@ -923,15 +936,15 @@ class Configuration implements ConfigurationInterface
;
}
private function addSerializerSection(ArrayNodeDefinition $rootNode)
private function addSerializerSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone, $willBeAvailable)
{
$rootNode
->children()
->arrayNode('serializer')
->info('serializer configuration')
->{!class_exists(FullStack::class) && class_exists(Serializer::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/serializer', Serializer::class)}()
->children()
->booleanNode('enable_annotations')->{!class_exists(FullStack::class) && class_exists(Annotation::class) ? 'defaultTrue' : 'defaultFalse'}()->end()
->booleanNode('enable_annotations')->{!class_exists(FullStack::class) && $willBeAvailable('doctrine/annotations', Annotation::class, 'symfony/serializer') ? 'defaultTrue' : 'defaultFalse'}()->end()
->scalarNode('name_converter')->end()
->scalarNode('circular_reference_handler')->end()
->scalarNode('max_depth_handler')->end()
@ -950,13 +963,14 @@ class Configuration implements ConfigurationInterface
;
}
private function addPropertyAccessSection(ArrayNodeDefinition $rootNode)
private function addPropertyAccessSection(ArrayNodeDefinition $rootNode, callable $willBeAvailable)
{
$rootNode
->children()
->arrayNode('property_access')
->addDefaultsIfNotSet()
->info('Property access configuration')
->{$willBeAvailable('symfony/property-access', PropertyAccessor::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->children()
->booleanNode('magic_call')->defaultFalse()->end()
->booleanNode('magic_get')->defaultTrue()->end()
@ -969,19 +983,19 @@ class Configuration implements ConfigurationInterface
;
}
private function addPropertyInfoSection(ArrayNodeDefinition $rootNode)
private function addPropertyInfoSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
->children()
->arrayNode('property_info')
->info('Property info configuration')
->{!class_exists(FullStack::class) && interface_exists(PropertyInfoExtractorInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/property-info', PropertyInfoExtractorInterface::class)}()
->end()
->end()
;
}
private function addCacheSection(ArrayNodeDefinition $rootNode)
private function addCacheSection(ArrayNodeDefinition $rootNode, callable $willBeAvailable)
{
$rootNode
->children()
@ -1008,7 +1022,7 @@ class Configuration implements ConfigurationInterface
->scalarNode('default_psr6_provider')->end()
->scalarNode('default_redis_provider')->defaultValue('redis://localhost')->end()
->scalarNode('default_memcached_provider')->defaultValue('memcached://localhost')->end()
->scalarNode('default_pdo_provider')->defaultValue(class_exists(Connection::class) ? 'database_connection' : null)->end()
->scalarNode('default_pdo_provider')->defaultValue($willBeAvailable('doctrine/dbal', Connection::class) ? 'database_connection' : null)->end()
->arrayNode('pools')
->useAttributeAsKey('name')
->prototype('array')
@ -1117,13 +1131,13 @@ class Configuration implements ConfigurationInterface
;
}
private function addLockSection(ArrayNodeDefinition $rootNode)
private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
->children()
->arrayNode('lock')
->info('Lock configuration')
->{!class_exists(FullStack::class) && class_exists(Lock::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/lock', Lock::class)}()
->beforeNormalization()
->ifString()->then(function ($v) { return ['enabled' => true, 'resources' => $v]; })
->end()
@ -1179,25 +1193,25 @@ class Configuration implements ConfigurationInterface
;
}
private function addWebLinkSection(ArrayNodeDefinition $rootNode)
private function addWebLinkSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
->children()
->arrayNode('web_link')
->info('web links configuration')
->{!class_exists(FullStack::class) && class_exists(HttpHeaderSerializer::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/weblink', HttpHeaderSerializer::class)}()
->end()
->end()
;
}
private function addMessengerSection(ArrayNodeDefinition $rootNode)
private function addMessengerSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
->children()
->arrayNode('messenger')
->info('Messenger configuration')
->{!class_exists(FullStack::class) && interface_exists(MessageBusInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/messenger', MessageBusInterface::class)}()
->fixXmlConfig('transport')
->fixXmlConfig('bus', 'buses')
->validate()
@ -1392,13 +1406,13 @@ class Configuration implements ConfigurationInterface
;
}
private function addHttpClientSection(ArrayNodeDefinition $rootNode)
private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
->children()
->arrayNode('http_client')
->info('HTTP Client configuration')
->{!class_exists(FullStack::class) && class_exists(HttpClient::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/http-client', HttpClient::class)}()
->fixXmlConfig('scoped_client')
->beforeNormalization()
->always(function ($config) {
@ -1728,13 +1742,13 @@ class Configuration implements ConfigurationInterface
;
}
private function addMailerSection(ArrayNodeDefinition $rootNode)
private function addMailerSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
->children()
->arrayNode('mailer')
->info('Mailer configuration')
->{!class_exists(FullStack::class) && class_exists(Mailer::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/mailer', Mailer::class)}()
->validate()
->ifTrue(function ($v) { return isset($v['dsn']) && \count($v['transports']); })
->thenInvalid('"dsn" and "transports" cannot be used together.')
@ -1784,13 +1798,13 @@ class Configuration implements ConfigurationInterface
;
}
private function addNotifierSection(ArrayNodeDefinition $rootNode)
private function addNotifierSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
->children()
->arrayNode('notifier')
->info('Notifier configuration')
->{!class_exists(FullStack::class) && class_exists(Notifier::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/notifier', Notifier::class)}()
->fixXmlConfig('chatter_transport')
->children()
->arrayNode('chatter_transports')
@ -1833,13 +1847,13 @@ class Configuration implements ConfigurationInterface
;
}
private function addRateLimiterSection(ArrayNodeDefinition $rootNode)
private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
->children()
->arrayNode('rate_limiter')
->info('Rate limiter configuration')
->{!class_exists(FullStack::class) && class_exists(TokenBucketLimiter::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/rate-limiter', TokenBucketLimiter::class)}()
->fixXmlConfig('limiter')
->beforeNormalization()
->ifTrue(function ($v) { return \is_array($v) && !isset($v['limiters']) && !isset($v['limiter']); })
@ -1901,13 +1915,13 @@ class Configuration implements ConfigurationInterface
;
}
private function addUidSection(ArrayNodeDefinition $rootNode)
private function addUidSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
->children()
->arrayNode('uid')
->info('Uid configuration')
->{class_exists(UuidFactory::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->{$enableIfStandalone('symfony/uid', UuidFactory::class)}()
->addDefaultsIfNotSet()
->children()
->enumNode('default_uuid_version')

View File

@ -14,6 +14,7 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection;
use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\Reader;
use Http\Client\HttpClient;
use phpDocumentor\Reflection\DocBlockFactoryInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Container\ContainerInterface as PsrContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;
@ -63,6 +64,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormTypeExtensionInterface;
use Symfony\Component\Form\FormTypeGuesserInterface;
use Symfony\Component\Form\FormTypeInterface;
@ -149,6 +151,7 @@ use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\CacheStorage;
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
use Symfony\Component\Routing\Loader\AnnotationFileLoader;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
@ -167,6 +170,7 @@ use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
use Symfony\Component\Validator\ObjectInitializerInterface;
use Symfony\Component\Validator\Validation;
use Symfony\Component\WebLink\HttpHeaderSerializer;
use Symfony\Component\Workflow;
use Symfony\Component\Workflow\WorkflowInterface;
@ -200,6 +204,7 @@ class FrameworkExtension extends Extension
private $mailerConfigEnabled = false;
private $httpClientConfigEnabled = false;
private $notifierConfigEnabled = false;
private $propertyAccessConfigEnabled = false;
private $lockConfigEnabled = false;
/**
@ -216,7 +221,7 @@ class FrameworkExtension extends Extension
$loader->load('fragment_renderer.php');
$loader->load('error_renderer.php');
if (interface_exists(PsrEventDispatcherInterface::class)) {
if (ContainerBuilder::willBeAvailable('psr/event-dispatcher', PsrEventDispatcherInterface::class, ['symfony/framework-bundle'])) {
$container->setAlias(PsrEventDispatcherInterface::class, 'event_dispatcher');
}
@ -256,11 +261,11 @@ class FrameworkExtension extends Extension
}
// If the slugger is used but the String component is not available, we should throw an error
if (!interface_exists(SluggerInterface::class)) {
if (!ContainerBuilder::willBeAvailable('symfony/string', SluggerInterface::class, ['symfony/framework-bundle'])) {
$container->register('slugger', 'stdClass')
->addError('You cannot use the "slugger" service since the String component is not installed. Try running "composer require symfony/string".');
} else {
if (!interface_exists(LocaleAwareInterface::class)) {
if (!ContainerBuilder::willBeAvailable('symfony/translation', LocaleAwareInterface::class, ['symfony/framework-bundle'])) {
$container->register('slugger', 'stdClass')
->addError('You cannot use the "slugger" service since the Translation contracts are not installed. Try running "composer require symfony/translation".');
}
@ -329,19 +334,19 @@ class FrameworkExtension extends Extension
}
if (null === $config['csrf_protection']['enabled']) {
$config['csrf_protection']['enabled'] = $this->sessionConfigEnabled && !class_exists(FullStack::class) && interface_exists(CsrfTokenManagerInterface::class);
$config['csrf_protection']['enabled'] = $this->sessionConfigEnabled && !class_exists(FullStack::class) && ContainerBuilder::willBeAvailable('symfony/security-csrf', CsrfTokenManagerInterface::class, ['symfony/framework-bundle']);
}
$this->registerSecurityCsrfConfiguration($config['csrf_protection'], $container, $loader);
if ($this->isConfigEnabled($container, $config['form'])) {
if (!class_exists(\Symfony\Component\Form\Form::class)) {
if (!class_exists(Form::class)) {
throw new LogicException('Form support cannot be enabled as the Form component is not installed. Try running "composer require symfony/form".');
}
$this->formConfigEnabled = true;
$this->registerFormConfiguration($config, $container, $loader);
if (class_exists(\Symfony\Component\Validator\Validation::class)) {
if (ContainerBuilder::willBeAvailable('symfony/validator', Validation::class, ['symfony/framework-bundle', 'symfony/form'])) {
$config['validation']['enabled'] = true;
} else {
$container->setParameter('validator.translation_domain', 'validators');
@ -469,7 +474,7 @@ class FrameworkExtension extends Extension
'Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController',
]);
if (class_exists(MimeTypes::class)) {
if (ContainerBuilder::willBeAvailable('symfony/mime', MimeTypes::class, ['symfony/framework-bundle'])) {
$loader->load('mime_type.php');
}
@ -602,7 +607,7 @@ class FrameworkExtension extends Extension
$container->setParameter('form.type_extension.csrf.enabled', false);
}
if (!class_exists(Translator::class)) {
if (!ContainerBuilder::willBeAvailable('symfony/translation', Translator::class, ['symfony/framework-bundle', 'symfony/form'])) {
$container->removeDefinition('form.type_extension.upload.validator');
}
if (!method_exists(CachingFactoryDecorator::class, 'reset')) {
@ -986,7 +991,7 @@ class FrameworkExtension extends Extension
$container->getDefinition('routing.loader')->replaceArgument(2, ['_locale' => $enabledLocales]);
}
if (!class_exists(ExpressionLanguage::class)) {
if (!ContainerBuilder::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/framework-bundle', 'symfony/routing'])) {
$container->removeDefinition('router.expression_language_provider');
}
@ -1225,18 +1230,18 @@ class FrameworkExtension extends Extension
$dirs = [];
$transPaths = [];
$nonExistingDirs = [];
if (class_exists(\Symfony\Component\Validator\Validation::class)) {
$r = new \ReflectionClass(\Symfony\Component\Validator\Validation::class);
if (ContainerBuilder::willBeAvailable('symfony/validator', Validation::class, ['symfony/framework-bundle', 'symfony/translation'])) {
$r = new \ReflectionClass(Validation::class);
$dirs[] = $transPaths[] = \dirname($r->getFileName()).'/Resources/translations';
}
if (class_exists(\Symfony\Component\Form\Form::class)) {
$r = new \ReflectionClass(\Symfony\Component\Form\Form::class);
if (ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle', 'symfony/translation'])) {
$r = new \ReflectionClass(Form::class);
$dirs[] = $transPaths[] = \dirname($r->getFileName()).'/Resources/translations';
}
if (class_exists(\Symfony\Component\Security\Core\Exception\AuthenticationException::class)) {
$r = new \ReflectionClass(\Symfony\Component\Security\Core\Exception\AuthenticationException::class);
if (ContainerBuilder::willBeAvailable('symfony/security-core', AuthenticationException::class, ['symfony/framework-bundle', 'symfony/translation'])) {
$r = new \ReflectionClass(AuthenticationException::class);
$dirs[] = $transPaths[] = \dirname($r->getFileName(), 2).'/Resources/translations';
}
@ -1336,7 +1341,7 @@ class FrameworkExtension extends Extension
return;
}
if (!class_exists(\Symfony\Component\Validator\Validation::class)) {
if (!class_exists(Validation::class)) {
throw new LogicException('Validation support cannot be enabled as the Validator component is not installed. Try running "composer require symfony/validator".');
}
@ -1403,8 +1408,8 @@ class FrameworkExtension extends Extension
$files['yaml' === $extension ? 'yml' : $extension][] = $path;
};
if (interface_exists(\Symfony\Component\Form\FormInterface::class)) {
$reflClass = new \ReflectionClass(\Symfony\Component\Form\FormInterface::class);
if (ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle', 'symfony/validator'])) {
$reflClass = new \ReflectionClass(Form::class);
$fileRecorder('xml', \dirname($reflClass->getFileName()).'/Resources/config/validation.xml');
}
@ -1466,7 +1471,7 @@ class FrameworkExtension extends Extension
}
if (!class_exists(\Doctrine\Common\Annotations\Annotation::class)) {
throw new LogicException('Annotations cannot be enabled as the Doctrine Annotation library is not installed.');
throw new LogicException('Annotations cannot be enabled as the Doctrine Annotation library is not installed. Try running "composer require doctrine/annotations".');
}
$loader->load('annotations.php');
@ -1478,7 +1483,7 @@ class FrameworkExtension extends Extension
if ('none' !== $config['cache']) {
if (!class_exists(\Doctrine\Common\Cache\CacheProvider::class)) {
throw new LogicException('Annotations cannot be enabled as the Doctrine Cache library is not installed.');
throw new LogicException('Annotations cannot be cached as the Doctrine Cache library is not installed. Try running "composer require doctrine/cache".');
}
$cacheService = $config['cache'];
@ -1521,7 +1526,7 @@ class FrameworkExtension extends Extension
private function registerPropertyAccessConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
{
if (!class_exists(PropertyAccessor::class)) {
if (!$this->propertyAccessConfigEnabled = $this->isConfigEnabled($container, $config)) {
return;
}
@ -1570,7 +1575,7 @@ class FrameworkExtension extends Extension
throw new InvalidArgumentException(sprintf('Invalid value "%s" set as "decryption_env_var": only "word" characters are allowed.', $config['decryption_env_var']));
}
if (class_exists(LazyString::class)) {
if (ContainerBuilder::willBeAvailable('symfony/string', LazyString::class, ['symfony/framework-bundle'])) {
$container->getDefinition('secrets.decryption_key')->replaceArgument(1, $config['decryption_env_var']);
} else {
$container->getDefinition('secrets.vault')->replaceArgument(1, "%env({$config['decryption_env_var']})%");
@ -1610,7 +1615,7 @@ class FrameworkExtension extends Extension
$chainLoader = $container->getDefinition('serializer.mapping.chain_loader');
if (!class_exists(PropertyAccessor::class)) {
if (!$this->propertyAccessConfigEnabled) {
$container->removeAlias('serializer.property_accessor');
$container->removeDefinition('serializer.normalizer.object');
}
@ -1619,7 +1624,7 @@ class FrameworkExtension extends Extension
$container->removeDefinition('serializer.encoder.yaml');
}
if (!class_exists(UnwrappingDenormalizer::class) || !class_exists(PropertyAccessor::class)) {
if (!class_exists(UnwrappingDenormalizer::class) || !$this->propertyAccessConfigEnabled) {
$container->removeDefinition('serializer.denormalizer.unwrapping');
}
@ -1703,7 +1708,7 @@ class FrameworkExtension extends Extension
$loader->load('property_info.php');
if (interface_exists(\phpDocumentor\Reflection\DocBlockFactoryInterface::class)) {
if (ContainerBuilder::willBeAvailable('phpdocumentor/reflection-docblock', DocBlockFactoryInterface::class, ['symfony/framework-bundle', 'symfony/property-info'])) {
$definition = $container->register('property_info.php_doc_extractor', 'Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor');
$definition->addTag('property_info.description_extractor', ['priority' => -1000]);
$definition->addTag('property_info.type_extractor', ['priority' => -1001]);
@ -1784,19 +1789,19 @@ class FrameworkExtension extends Extension
$loader->load('messenger.php');
if (class_exists(AmqpTransportFactory::class)) {
if (ContainerBuilder::willBeAvailable('symfony/amqp-messenger', AmqpTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) {
$container->getDefinition('messenger.transport.amqp.factory')->addTag('messenger.transport_factory');
}
if (class_exists(RedisTransportFactory::class)) {
if (ContainerBuilder::willBeAvailable('symfony/redis-messenger', RedisTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) {
$container->getDefinition('messenger.transport.redis.factory')->addTag('messenger.transport_factory');
}
if (class_exists(AmazonSqsTransportFactory::class)) {
if (ContainerBuilder::willBeAvailable('symfony/amazon-sqs-messenger', AmazonSqsTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) {
$container->getDefinition('messenger.transport.sqs.factory')->addTag('messenger.transport_factory');
}
if (class_exists(BeanstalkdTransportFactory::class)) {
if (ContainerBuilder::willBeAvailable('symfony/beanstalkd-messenger', BeanstalkdTransportFactory::class, ['symfony/framework-bundle', 'symfony/messenger'])) {
$container->getDefinition('messenger.transport.beanstalkd.factory')->addTag('messenger.transport_factory');
}
@ -2070,12 +2075,12 @@ class FrameworkExtension extends Extension
unset($options['retry_failed']);
$container->getDefinition('http_client')->setArguments([$options, $config['max_host_connections'] ?? 6]);
if (!$hasPsr18 = interface_exists(ClientInterface::class)) {
if (!$hasPsr18 = ContainerBuilder::willBeAvailable('psr/http-client', ClientInterface::class, ['symfony/framework-bundle', 'symfony/http-client'])) {
$container->removeDefinition('psr18.http_client');
$container->removeAlias(ClientInterface::class);
}
if (!interface_exists(HttpClient::class)) {
if (!ContainerBuilder::willBeAvailable('php-http/httplug', HttpClient::class, ['symfony/framework-bundle', 'symfony/http-client'])) {
$container->removeDefinition(HttpClient::class);
}
@ -2205,7 +2210,9 @@ class FrameworkExtension extends Extension
];
foreach ($classToServices as $class => $service) {
if (!class_exists($class)) {
$package = substr($service, \strlen('mailer.transport_factory.'));
if (!ContainerBuilder::willBeAvailable(sprintf('symfony/%s-mailer', 'gmail' === $package ? 'google' : $package), $class, ['symfony/framework-bundle', 'symfony/mailer'])) {
$container->removeDefinition($service);
}
}
@ -2302,16 +2309,26 @@ class FrameworkExtension extends Extension
ClickatellTransportFactory::class => 'notifier.transport_factory.clickatell',
];
$parentPackages = ['symfony/framework-bundle', 'symfony/notifier'];
foreach ($classToServices as $class => $service) {
if (!class_exists($class)) {
switch ($package = substr($service, \strlen('notifier.transport_factory.'))) {
case 'freemobile': $package = 'free-mobile'; break;
case 'googlechat': $package = 'google-chat'; break;
case 'linkedin': $package = 'linked-in'; break;
case 'ovhcloud': $package = 'ovh-cloud'; break;
case 'rocketchat': $package = 'rocket-chat'; break;
}
if (!ContainerBuilder::willBeAvailable(sprintf('symfony/%s-notifier', $package), $class, $parentPackages)) {
$container->removeDefinition($service);
}
}
if (class_exists(MercureTransportFactory::class) && class_exists(MercureBundle::class)) {
if (ContainerBuilder::willBeAvailable('symfony/mercure-notifier', MercureTransportFactory::class, $parentPackages) && ContainerBuilder::willBeAvailable('symfony/mercure-bundle', MercureBundle::class, $parentPackages)) {
$container->getDefinition($classToServices[MercureTransportFactory::class])
->replaceArgument('$publisherLocator', new ServiceLocatorArgument(new TaggedIteratorArgument('mercure.publisher', null, null, true)));
} elseif (class_exists(MercureTransportFactory::class)) {
} elseif (ContainerBuilder::willBeAvailable('symfony/mercure-notifier', MercureTransportFactory::class, $parentPackages)) {
$container->removeDefinition($classToServices[MercureTransportFactory::class]);
}

View File

@ -443,6 +443,7 @@ class ConfigurationTest extends TestCase
'mapping' => ['paths' => []],
],
'property_access' => [
'enabled' => true,
'magic_call' => false,
'magic_get' => true,
'magic_set' => true,
@ -566,7 +567,7 @@ class ConfigurationTest extends TestCase
'limiters' => [],
],
'uid' => [
'enabled' => class_exists(UuidFactory::class),
'enabled' => !class_exists(FullStack::class) && class_exists(UuidFactory::class),
'default_uuid_version' => 6,
'name_based_uuid_version' => 5,
'time_based_uuid_version' => 6,

View File

@ -11,6 +11,7 @@
namespace Symfony\Bundle\SecurityBundle\DependencyInjection;
use Symfony\Bridge\Twig\Extension\LogoutUrlExtension;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory;
@ -31,6 +32,7 @@ use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;
@ -42,7 +44,6 @@ use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
use Symfony\Component\Security\Core\User\ChainUserProvider;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
use Twig\Extension\AbstractExtension;
/**
* SecurityExtension.
@ -130,7 +131,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
$loader->load('security_legacy.php');
}
if (class_exists(AbstractExtension::class)) {
if ($container::willBeAvailable('symfony/twig-bridge', LogoutUrlExtension::class, ['symfony/security-bundle'])) {
$loader->load('templating_twig.php');
}
@ -141,7 +142,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
$loader->load('security_debug.php');
}
if (!class_exists(\Symfony\Component\ExpressionLanguage\ExpressionLanguage::class)) {
if (!$container::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/security-bundle'])) {
$container->removeDefinition('security.expression_language');
$container->removeDefinition('security.access.expression_voter');
}
@ -982,8 +983,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
return $this->expressions[$id];
}
if (!class_exists(\Symfony\Component\ExpressionLanguage\ExpressionLanguage::class)) {
throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
if (!$container::willBeAvailable('symfony/expression-language', ExpressionLanguage::class, ['symfony/security-bundle'])) {
throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
}
$container

View File

@ -19,7 +19,7 @@
"php": ">=7.2.5",
"ext-xml": "*",
"symfony/config": "^4.4|^5.0",
"symfony/dependency-injection": "^5.2",
"symfony/dependency-injection": "^5.3",
"symfony/deprecation-contracts": "^2.1",
"symfony/event-dispatcher": "^5.1",
"symfony/http-kernel": "^5.0",

View File

@ -11,10 +11,14 @@
namespace Symfony\Bundle\TwigBundle\DependencyInjection\Compiler;
use Symfony\Component\Asset\Packages;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Workflow\Workflow;
use Symfony\Component\Yaml\Yaml;
/**
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
@ -23,19 +27,19 @@ class ExtensionPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!class_exists(\Symfony\Component\Asset\Packages::class)) {
if (!$container::willBeAvailable('symfony/asset', Packages::class, ['symfony/twig-bundle'])) {
$container->removeDefinition('twig.extension.assets');
}
if (!class_exists(\Symfony\Component\ExpressionLanguage\Expression::class)) {
if (!$container::willBeAvailable('symfony/expression-language', Expression::class, ['symfony/twig-bundle'])) {
$container->removeDefinition('twig.extension.expression');
}
if (!interface_exists(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class)) {
if (!$container::willBeAvailable('symfony/routing', UrlGeneratorInterface::class, ['symfony/twig-bundle'])) {
$container->removeDefinition('twig.extension.routing');
}
if (!class_exists(\Symfony\Component\Yaml\Yaml::class)) {
if (!$container::willBeAvailable('symfony/yaml', Yaml::class, ['symfony/twig-bundle'])) {
$container->removeDefinition('twig.extension.yaml');
}
@ -111,7 +115,7 @@ class ExtensionPass implements CompilerPassInterface
$container->getDefinition('twig.extension.expression')->addTag('twig.extension');
}
if (!class_exists(Workflow::class) || !$container->has('workflow.registry')) {
if (!$container::willBeAvailable('symfony/workflow', Workflow::class, ['symfony/twig-bundle']) || !$container->has('workflow.registry')) {
$container->removeDefinition('workflow.twig_extension');
} else {
$container->getDefinition('workflow.twig_extension')->addTag('twig.extension');

View File

@ -17,6 +17,7 @@ use Symfony\Component\Console\Application;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Translation\Translator;
@ -37,19 +38,19 @@ class TwigExtension extends Extension
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('twig.php');
if (class_exists(\Symfony\Component\Form\Form::class)) {
if ($container::willBeAvailable('symfony/form', Form::class, ['symfony/twig-bundle'])) {
$loader->load('form.php');
}
if (class_exists(Application::class)) {
if ($container::willBeAvailable('symfony/console', Application::class, ['symfony/twig-bundle'])) {
$loader->load('console.php');
}
if (class_exists(Mailer::class)) {
if ($container::willBeAvailable('symfony/mailer', Mailer::class, ['symfony/twig-bundle'])) {
$loader->load('mailer.php');
}
if (!class_exists(Translator::class)) {
if (!$container::willBeAvailable('symfony/translation', Translator::class, ['symfony/twig-bundle'])) {
$container->removeDefinition('twig.translation.extractor');
}

View File

@ -27,7 +27,7 @@
"require-dev": {
"symfony/asset": "^4.4|^5.0",
"symfony/stopwatch": "^4.4|^5.0",
"symfony/dependency-injection": "^5.2",
"symfony/dependency-injection": "^5.3",
"symfony/expression-language": "^4.4|^5.0",
"symfony/finder": "^4.4|^5.0",
"symfony/form": "^4.4|^5.0",
@ -40,7 +40,7 @@
"doctrine/cache": "~1.0"
},
"conflict": {
"symfony/dependency-injection": "<5.2",
"symfony/dependency-injection": "<5.3",
"symfony/framework-bundle": "<5.0",
"symfony/translation": "<5.0"
},

View File

@ -9,6 +9,7 @@ CHANGELOG
* Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8
* Add autoconfigurable attributes
* Add support for per-env configuration in loaders
* Add `ContainerBuilder::willBeAvailable()` to help with conditional configuration
5.2.0
-----

View File

@ -198,7 +198,7 @@ abstract class AbstractRecursivePass implements CompilerPassInterface
{
if (null === $this->expressionLanguage) {
if (!class_exists(ExpressionLanguage::class)) {
throw new LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.');
throw new LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".');
}
$providers = $this->container->getExpressionLanguageProviders();

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\DependencyInjection;
use Composer\InstalledVersions;
use Psr\Container\ContainerInterface as PsrContainerInterface;
use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\Config\Resource\ComposerResource;
@ -1467,6 +1468,34 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
$this->getCompiler()->log($pass, $this->resolveEnvPlaceholders($message));
}
/**
* Checks whether a class is available and will remain available in the "no-dev" mode of Composer.
*
* When parent packages are provided and if any of them is in dev-only mode,
* the class will be considered available even if it is also in dev-only mode.
*/
final public static function willBeAvailable(string $package, string $class, array $parentPackages): bool
{
if (!class_exists($class) && !interface_exists($class, false) && !trait_exists($class, false)) {
return false;
}
if (!class_exists(InstalledVersions::class) || !InstalledVersions::isInstalled($package) || InstalledVersions::isInstalled($package, false)) {
return true;
}
// the package is installed but in dev-mode only, check if this applies to one of the parent packages too
$rootPackage = InstalledVersions::getRootPackage()['name'] ?? '';
foreach ($parentPackages as $parentPackage) {
if ($rootPackage === $parentPackage || (InstalledVersions::isInstalled($parentPackage) && !InstalledVersions::isInstalled($parentPackage, false))) {
return true;
}
}
return false;
}
/**
* Gets removed binding ids.
*