diff --git a/composer.json b/composer.json index b356f8d..04ccbd4 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "symfony/config": "^6.4|^7.3|^8.0", "symfony/dependency-injection": "^6.4|^7.3|^8.0", "symfony/http-kernel": "^6.4|^7.3|^8.0", - "symfony/mercure": "^0.6.1", + "symfony/mercure": "*", "symfony/web-link": "^6.4|^7.3|^8.0" }, "autoload": { diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index ee39bac..181a609 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -15,6 +15,7 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Mercure\FrankenPhpHub; /** * MercureExtension configuration structure. @@ -25,9 +26,12 @@ final class Configuration implements ConfigurationInterface { public function getConfigTreeBuilder(): TreeBuilder { + $builtinPublish = class_exists(FrankenPhpHub::class) && \function_exists('mercure_publish'); + $treeBuilder = new TreeBuilder('mercure'); $rootNode = $treeBuilder->getRootNode(); - $rootNode + + $urlNode = $rootNode ->fixXmlConfig('hub') ->children() ->arrayNode('hubs') @@ -35,58 +39,70 @@ public function getConfigTreeBuilder(): TreeBuilder ->normalizeKeys(false) ->arrayPrototype() ->children() - ->scalarNode('url')->info('URL of the hub\'s publish endpoint')->example('https://demo.mercure.rocks/.well-known/mercure')->end() - ->scalarNode('public_url')->info('URL of the hub\'s public endpoint')->example('https://demo.mercure.rocks/.well-known/mercure')->defaultNull()->end() - ->arrayNode('jwt') - ->beforeNormalization() - ->ifString() - ->then(static function (string $token): array { - return [ - 'value' => $token, - ]; - }) - ->end() - ->info('JSON Web Token configuration.') - ->children() - ->scalarNode('value')->info('JSON Web Token to use to publish to this hub.')->end() - ->scalarNode('provider')->info('The ID of a service to call to provide the JSON Web Token.')->end() - ->scalarNode('factory')->info('The ID of a service to call to create the JSON Web Token.')->end() - ->arrayNode('publish') - ->beforeNormalization()->castToArray()->end() - ->scalarPrototype()->end() - ->info('A list of topics to allow publishing to when using the given factory to generate the JWT.') - ->end() - ->arrayNode('subscribe') - ->beforeNormalization()->castToArray()->end() - ->scalarPrototype()->end() - ->info('A list of topics to allow subscribing to when using the given factory to generate the JWT.') - ->end() - ->scalarNode('secret')->info('The JWT Secret to use.')->example('!ChangeMe!')->end() - ->scalarNode('passphrase')->info('The JWT secret passphrase.')->defaultValue('')->end() - ->scalarNode('algorithm')->info('The algorithm to use to sign the JWT')->defaultValue('hmac.sha256')->end() - ->end() - ->end() - ->scalarNode('jwt_provider') - ->info('The ID of a service to call to generate the JSON Web Token.') - ->setDeprecated('symfony/mercure-bundle', '0.3', 'The child node "%node%" at path "%path%" is deprecated, use "jwt.provider" instead.') - ->end() - ->scalarNode('bus')->info('Name of the Messenger bus where the handler for this hub must be registered. Default to the default bus if Messenger is enabled.')->end() + ->scalarNode('url')->info('URL of the hub\'s publish endpoint')->example('https://demo.mercure.rocks/.well-known/mercure'); + + if ($builtinPublish) { + $urlNode->defaultNull(); + } + + $publicUrlNode = $urlNode->end() + ->scalarNode('public_url')->info('URL of the hub\'s public endpoint')->example('https://demo.mercure.rocks/.well-known/mercure'); + + if (!$builtinPublish) { + $publicUrlNode->defaultNull(); + } + + $publicUrlNode->end() + ->arrayNode('jwt') + ->beforeNormalization() + ->ifString() + ->then(static function (string $token): array { + return [ + 'value' => $token, + ]; + }) + ->end() + ->info('JSON Web Token configuration.') + ->children() + ->scalarNode('value')->info('JSON Web Token to use to publish to this hub.')->end() + ->scalarNode('provider')->info('The ID of a service to call to provide the JSON Web Token.')->end() + ->scalarNode('factory')->info('The ID of a service to call to create the JSON Web Token.')->end() + ->arrayNode('publish') + ->beforeNormalization()->castToArray()->end() + ->scalarPrototype()->end() + ->info('A list of topics to allow publishing to when using the given factory to generate the JWT.') + ->end() + ->arrayNode('subscribe') + ->beforeNormalization()->castToArray()->end() + ->scalarPrototype()->end() + ->info('A list of topics to allow subscribing to when using the given factory to generate the JWT.') + ->end() + ->scalarNode('secret')->info('The JWT Secret to use.')->example('!ChangeMe!')->end() + ->scalarNode('passphrase')->info('The JWT secret passphrase.')->defaultValue('')->end() + ->scalarNode('algorithm')->info('The algorithm to use to sign the JWT')->defaultValue('hmac.sha256')->end() + ->end() + ->end() + ->scalarNode('jwt_provider') + ->info('The ID of a service to call to generate the JSON Web Token.') + ->setDeprecated('symfony/mercure-bundle', '0.3', 'The child node "%node%" at path "%path%" is deprecated, use "jwt.provider" instead.') + ->end() + ->scalarNode('bus')->info('Name of the Messenger bus where the handler for this hub must be registered. Default to the default bus if Messenger is enabled.')->end() ->end() ->validate() - ->ifTrue(function ($v) { return isset($v['jwt'], $v['jwt_provider']); }) - ->thenInvalid('"jwt" and "jwt_provider" cannot be used together.') + ->ifTrue(function ($v) { return isset($v['jwt'], $v['jwt_provider']); }) + ->thenInvalid('"jwt" and "jwt_provider" cannot be used together.') ->end() ->validate() - ->ifTrue(function ($v) { return !isset($v['jwt']) && !isset($v['jwt_provider']); }) - ->thenInvalid('You must specify at least one of "jwt", and "jwt_provider".') + ->ifTrue(function ($v) { return isset($v['url']) && !isset($v['jwt']) && !isset($v['jwt_provider']); }) + ->thenInvalid('You must specify at least one of "jwt", and "jwt_provider".') ->end() ->validate() - ->ifTrue(function ($v) { return isset($v['jwt']['value'], $v['jwt']['provider']); }) - ->thenInvalid('"jwt.value" and "jwt.provider" cannot be used together.') + ->ifTrue(function ($v) { return isset($v['jwt']['value'], $v['jwt']['provider']); }) + ->thenInvalid('"jwt.value" and "jwt.provider" cannot be used together.') ->end() ->validate() - ->ifTrue(function ($v) { return isset($v['jwt']) && !isset($v['jwt']['value']) && !isset($v['jwt']['provider']) && !isset($v['jwt']['factory']) && !isset($v['jwt']['secret']); }) - ->thenInvalid('You must specify at least one of "jwt.value", "jwt.provider", "jwt.factory", and "jwt.secret".') + ->ifTrue(function ($v) { return isset($v['jwt']) && !isset($v['jwt']['value']) && !isset($v['jwt']['provider']) && !isset($v['jwt']['factory']) && !isset($v['jwt']['secret']); }) + ->thenInvalid('You must specify at least one of "jwt.value", "jwt.provider", "jwt.factory", and "jwt.secret".') ->end() ->end() ->end() diff --git a/src/DependencyInjection/MercureExtension.php b/src/DependencyInjection/MercureExtension.php index b1adb26..83372ca 100644 --- a/src/DependencyInjection/MercureExtension.php +++ b/src/DependencyInjection/MercureExtension.php @@ -28,6 +28,7 @@ use Symfony\Component\Mercure\Debug\TraceablePublisher; use Symfony\Component\Mercure\Discovery; use Symfony\Component\Mercure\EventSubscriber\SetCookieSubscriber; +use Symfony\Component\Mercure\FrankenPhpHub; use Symfony\Component\Mercure\Hub; use Symfony\Component\Mercure\HubInterface; use Symfony\Component\Mercure\HubRegistry; @@ -66,23 +67,22 @@ public function load(array $configs, ContainerBuilder $container): void $defaultPublisher = null; $defaultHubId = null; - $hubUrls = []; $traceableHubs = []; $hubs = []; - $defaultHubUrl = null; $defaultHubName = null; $enableProfiler = ($config['enable_profiler'] ?? $container->getParameter('kernel.debug')) && class_exists(Stopwatch::class); foreach ($config['hubs'] as $name => $hub) { + $builtinHub = !isset($hub['url']); + $tokenFactory = null; + $tokenProvider = null; if (isset($hub['jwt'])) { - $tokenProvider = null; if (isset($hub['jwt']['value'])) { $tokenProvider = \sprintf('mercure.hub.%s.jwt.provider', $name); $container->register($tokenProvider, StaticTokenProvider::class) ->addArgument($hub['jwt']['value']) - ->addTag('mercure.jwt.provider') - ; + ->addTag('mercure.jwt.provider'); // TODO: remove the following definition in 0.4 $jwtProvider = \sprintf('mercure.hub.%s.jwt_provider', $name); @@ -106,14 +106,12 @@ public function load(array $configs, ContainerBuilder $container): void ->addArgument($hub['jwt']['algorithm']) ->addArgument(null) ->addArgument($hub['jwt']['passphrase']) - ->addTag('mercure.jwt.factory') - ; + ->addTag('mercure.jwt.factory'); } $container->register('.lazy.'.$tokenFactory, TokenFactoryInterface::class) ->setFactory(['Closure', 'fromCallable']) - ->addArgument([new Reference($tokenFactory), 'create']) - ; + ->addArgument([new Reference($tokenFactory), 'create']); $tokenFactory = '.lazy.'.$tokenFactory; $tokenProvider = \sprintf('mercure.hub.%s.jwt.provider', $name); @@ -121,71 +119,80 @@ public function load(array $configs, ContainerBuilder $container): void ->addArgument(new Reference($tokenFactory)) ->addArgument($hub['jwt']['subscribe'] ?? []) ->addArgument($hub['jwt']['publish'] ?? []) - ->addTag('mercure.jwt.factory') - ; + ->addTag('mercure.jwt.factory'); $container->registerAliasForArgument($tokenFactory, TokenFactoryInterface::class, $name); $container->registerAliasForArgument($tokenFactory, TokenFactoryInterface::class, "{$name}Factory"); - $container->registerAliasForArgument($tokenFactory, TokenFactoryInterface::class, "{$name}TokenFactory"); + $container->registerAliasForArgument( + $tokenFactory, + TokenFactoryInterface::class, + "{$name}TokenFactory" + ); } - } else { + } elseif (isset($hub['jwt_provider'])) { $jwtProvider = $hub['jwt_provider']; $tokenProvider = \sprintf('mercure.hub.%s.jwt.provider', $name); $container->register($tokenProvider, CallableTokenProvider::class) ->addArgument(new Reference($jwtProvider)) - ->addTag('mercure.jwt.provider') - ; + ->addTag('mercure.jwt.provider'); } - $container->registerAliasForArgument($tokenProvider, TokenProviderInterface::class, $name); - $container->registerAliasForArgument($tokenProvider, TokenProviderInterface::class, "{$name}Provider"); - $container->registerAliasForArgument($tokenProvider, TokenProviderInterface::class, "{$name}TokenProvider"); + if (null !== $tokenProvider) { + $container->registerAliasForArgument($tokenProvider, TokenProviderInterface::class, $name); + $container->registerAliasForArgument($tokenProvider, TokenProviderInterface::class, "{$name}Provider"); + $container->registerAliasForArgument($tokenProvider, TokenProviderInterface::class, "{$name}TokenProvider"); + } - $hubUrls[$name] = $hub['url']; $hubId = \sprintf('mercure.hub.%s', $name); $publisherId = \sprintf('mercure.hub.%s.publisher', $name); $hubs[$name] = new Reference($hubId); if (!$defaultPublisher && ($config['default_hub'] ?? $name) === $name) { $defaultHubName = $name; $defaultHubId = $hubId; - $defaultHubUrl = $hub['url']; $defaultPublisher = $publisherId; } - $container->register($hubId, Hub::class) - ->addArgument($hub['url']) - ->addArgument(new Reference($tokenProvider)) - ->addArgument($tokenFactory ? new Reference($tokenFactory) : null) - ->addArgument($hub['public_url']) - ->addArgument(new Reference('http_client', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)) - ->addTag('mercure.hub') - ; - - $container->registerAliasForArgument($hubId, HubInterface::class, "{$name}Hub"); - $container->registerAliasForArgument($hubId, HubInterface::class, $name); - - $publisherDefinition = $container->register($publisherId, Publisher::class) - ->addArgument($hub['url']) - ->addArgument(new Reference($tokenProvider)) - ->addArgument(new Reference('http_client', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)) - ->addTag('mercure.publisher') - ; - - $this->deprecate( - $publisherDefinition, - 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future, use "'.$hubId.'" instead.' - ); - - $this->deprecate( - $container->registerAliasForArgument($publisherId, PublisherInterface::class, "{$name}Publisher"), - 'The "%alias_id%" service is deprecated. You should stop using it, as it will be removed in the future, use "'.$hubId.'" instead.' - ); + if ($builtinHub) { + $container->register($hubId, FrankenPhpHub::class) + ->addArgument($hub['public_url']) + ->addArgument($tokenFactory ? new Reference($tokenFactory) : null) + ->addTag('mercure.hub'); + } else { + $container->register($hubId, Hub::class) + ->addArgument($hub['url']) + ->addArgument(new Reference($tokenProvider)) + ->addArgument($tokenFactory ? new Reference($tokenFactory) : null) + ->addArgument($hub['public_url']) + ->addArgument(new Reference('http_client', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)) + ->addTag('mercure.hub'); + } - $this->deprecate( - $container->registerAliasForArgument($publisherId, PublisherInterface::class, $name), - 'The "%alias_id%" service is deprecated. You should stop using it, as it will be removed in the future, use "'.$hubId.'" instead.' - ); + if (!$builtinHub) { + $container->registerAliasForArgument($hubId, HubInterface::class, "{$name}Hub"); + $container->registerAliasForArgument($hubId, HubInterface::class, $name); + + $publisherDefinition = $container->register($publisherId, Publisher::class) + ->addArgument($hub['url']) + ->addArgument(new Reference($tokenProvider)) + ->addArgument(new Reference('http_client', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)) + ->addTag('mercure.publisher'); + + $this->deprecate( + $publisherDefinition, + 'The "%service_id%" service is deprecated. You should stop using it, as it will be removed in the future, use "'.$hubId.'" instead.' + ); + + $this->deprecate( + $container->registerAliasForArgument($publisherId, PublisherInterface::class, "{$name}Publisher"), + 'The "%alias_id%" service is deprecated. You should stop using it, as it will be removed in the future, use "'.$hubId.'" instead.' + ); + + $this->deprecate( + $container->registerAliasForArgument($publisherId, PublisherInterface::class, $name), + 'The "%alias_id%" service is deprecated. You should stop using it, as it will be removed in the future, use "'.$hubId.'" instead.' + ); + } $bus = $hub['bus'] ?? null; $attributes = null === $bus ? [] : ['bus' => $hub['bus']]; @@ -196,14 +203,19 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('messenger.message_handler', $attributes); if ($enableProfiler) { - $traceablePublisher = $container->register("$publisherId.traceable", TraceablePublisher::class) - ->setDecoratedService($publisherId) - ->addArgument(new Reference("$publisherId.traceable.inner")) - ->addArgument(new Reference('debug.stopwatch')); + if (!$builtinHub) { + $traceablePublisher = $container->register("$publisherId.traceable", TraceablePublisher::class) + ->setDecoratedService($publisherId) + ->addArgument(new Reference("$publisherId.traceable.inner")) + ->addArgument(new Reference('debug.stopwatch')); - $this->deprecate($traceablePublisher, 'The "%service_id%" service is deprecated. Use "'.$hubId.'.traceable" instead.'); + $this->deprecate( + $traceablePublisher, + 'The "%service_id%" service is deprecated. Use "'.$hubId.'.traceable" instead.' + ); - $traceableHubs[$name] = new Reference("$publisherId.traceable"); + $traceableHubs[$name] = new Reference("$publisherId.traceable"); + } $container->register("$hubId.traceable", TraceableHub::class) ->setDecoratedService($hubId) @@ -245,15 +257,17 @@ public function load(array $configs, ContainerBuilder $container): void $container->setAlias(HubInterface::class, $defaultHubId); - $this->deprecate( - $container->setAlias(Publisher::class, $defaultPublisher), - 'The "%alias_id%" service alias is deprecated. Use "'.Hub::class.'" instead.' - ); + if (null !== $defaultPublisher) { + $this->deprecate( + $container->setAlias(Publisher::class, $defaultPublisher), + 'The "%alias_id%" service alias is deprecated. Use "'.Hub::class.'" instead.' + ); - $this->deprecate( - $container->setAlias(PublisherInterface::class, $defaultPublisher), - 'The "%alias_id%" service alias is deprecated. Use "'.HubInterface::class.'" instead.' - ); + $this->deprecate( + $container->setAlias(PublisherInterface::class, $defaultPublisher), + 'The "%alias_id%" service alias is deprecated. Use "'.HubInterface::class.'" instead.' + ); + } $container->register(HubRegistry::class) ->addArgument(new Reference($defaultHubId)) @@ -279,10 +293,6 @@ public function load(array $configs, ContainerBuilder $container): void ->setArguments([new Reference(HubRegistry::class), new Reference(Authorization::class), new Reference('request_stack')]) ->addTag('twig.extension'); } - - // TODO: remove these parameters in the next release. - $container->setParameter('mercure.hubs', $hubUrls); - $container->setParameter('mercure.default_hub', $defaultHubUrl); } /** diff --git a/tests/DependencyInjection/MercureExtensionTest.php b/tests/DependencyInjection/MercureExtensionTest.php index 8e711e4..54fc4ba 100644 --- a/tests/DependencyInjection/MercureExtensionTest.php +++ b/tests/DependencyInjection/MercureExtensionTest.php @@ -17,6 +17,8 @@ use Symfony\Bundle\MercureBundle\DependencyInjection\MercureExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\Mercure\FrankenPhpHub; +use Symfony\Component\Mercure\Hub; use Symfony\Component\Mercure\HubRegistry; /** @@ -198,8 +200,6 @@ public function testExtensionLegacy() $this->assertSame('https://demo.mercure.rocks/hub', $container->getDefinition('mercure.hub.default')->getArgument(0)); $this->assertArrayHasKey('mercure.publisher', $container->getDefinition('mercure.hub.default.publisher')->getTags()); $this->assertSame('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.HB0k08BaV8KlLZ3EafCRlTDGbkd9qdznCzJQ_l8ELTU', $container->getDefinition('mercure.hub.default.jwt_provider')->getArgument(0)); - $this->assertSame(['default' => 'https://demo.mercure.rocks/hub', 'managed' => 'https://demo.mercure.rocks/managed'], $container->getParameter('mercure.hubs')); - $this->assertSame('https://demo.mercure.rocks/hub', $container->getParameter('mercure.default_hub')); $this->assertArrayHasKey('Symfony\Component\Mercure\PublisherInterface $defaultPublisher', $container->getAliases()); $this->assertArrayHasKey('Symfony\Component\Mercure\PublisherInterface $managedPublisher', $container->getAliases()); @@ -212,4 +212,35 @@ public function testExtensionLegacy() $this->assertSame($config['mercure']['hubs'][0]['url'], $registry->getHub('default')->getUrl()); $this->assertSame($config['mercure']['hubs'][1]['url'], $registry->getHub('managed')->getUrl()); } + + public function testExtensionBuiltin() + { + if (!class_exists(FrankenPhpHub::class)) { + $this->markTestSkipped('FrankenPhpHub is not available (old version of symfony/mercure).'); + } + + $config = [ + 'mercure' => [ + 'hubs' => [ + [ + 'name' => 'default', + 'public_url' => 'https://demo.mercure.rocks/hub', + ], + ], + ], + ]; + + $container = new ContainerBuilder(new ParameterBag(['kernel.debug' => false])); + (new MercureExtension())->load($config, $container); + + $this->assertTrue($container->hasDefinition('mercure.hub.default')); + $this->assertSame(FrankenPhpHub::class, $container->getDefinition('mercure.hub.default')->getClass()); + } +} + +// Stub for mercure_publish() +if (!\function_exists('mercure_publish')) { + function mercure_publish() + { + } }