diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c38ab35..07f76616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. +### Added + +- Integration for VCR Plugin + ## 1.15.2 - 2019-04-18 ### Fixed diff --git a/composer.json b/composer.json index ffac6324..479bea70 100644 --- a/composer.json +++ b/composer.json @@ -38,8 +38,9 @@ "nyholm/nsa": "^1.1", "php-http/cache-plugin": "^1.6", "php-http/guzzle6-adapter": "^1.1.1 || ^2.0.1", - "php-http/promise": "^1.0", "php-http/mock-client": "^1.2", + "php-http/promise": "^1.0", + "php-http/vcr-plugin": "^1.0@dev", "polishsymfonycommunity/symfony-mocker-container": "^1.0", "symfony/browser-kit": "^2.8.49 || ^3.0.9 || ^3.1.10 || ^3.2.14 || ^3.3.18 || ^3.4.20 || ^4.0.15 || ^4.1.9 || ^4.2.1", "symfony/cache": "^3.1.10 || ^3.2.14 || ^3.3.18 || ^3.4.20 || ^4.0.15 || ^4.1.9 || ^4.2.1", diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 0bcfd928..d5027de4 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -5,6 +5,9 @@ use Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator; use Http\Client\Common\Plugin\CachePlugin; use Http\Client\Common\Plugin\Journal; +use Http\Client\Plugin\Vcr\NamingStrategy\NamingStrategyInterface; +use Http\Client\Plugin\Vcr\Recorder\PlayerInterface; +use Http\Client\Plugin\Vcr\Recorder\RecorderInterface; use Http\Message\CookieJar; use Http\Message\Formatter; use Http\Message\StreamFactory; @@ -437,6 +440,50 @@ private function createClientPluginNode() ->end() ->end() ->end() + ->arrayNode('vcr') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->info('Record response to be replayed during tests or development cycle.') + ->validate() + ->ifTrue(function ($config) { + return 'filesystem' === $config['recorder'] && empty($config['fixtures_directory']); + }) + ->thenInvalid('If you want to use the "filesystem" recorder you must also specify a "fixtures_directory".') + ->end() + ->children() + ->enumNode('mode') + ->info('What should be the behavior of the plugin?') + ->values(['record', 'replay', 'replay_or_record']) + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('recorder') + ->info(sprintf('Which recorder to use. Can be "in_memory", "filesystem" or the ID of your service implementing %s and %s. When using filesystem, specify "fixtures_directory" as well.', RecorderInterface::class, PlayerInterface::class)) + ->defaultValue('filesystem') + ->cannotBeEmpty() + ->end() + ->scalarNode('naming_strategy') + ->info(sprintf('Which naming strategy to use. Add the ID of your service implementing %s to override the default one.', NamingStrategyInterface::class)) + ->defaultValue('default') + ->cannotBeEmpty() + ->end() + ->arrayNode('naming_strategy_options') + ->info('See http://docs.php-http.org/en/latest/plugins/vcr.html#the-naming-strategy for more details') + ->children() + ->arrayNode('hash_headers') + ->info('List of header(s) that make the request unique (Ex: ‘Authorization’)') + ->prototype('scalar')->end() + ->end() + ->arrayNode('hash_body_methods') + ->info('for which request methods the body makes requests distinct.') + ->prototype('scalar')->end() + ->end() + ->end() + ->end() // End naming_strategy_options + ->scalarNode('fixtures_directory') + ->info('Where the responses will be stored and replay from when using the filesystem recorder. Should be accessible to your VCS.') + ->end() + ->end() ->end() ->end(); diff --git a/src/DependencyInjection/HttplugExtension.php b/src/DependencyInjection/HttplugExtension.php index 78595c27..faf1e1d5 100644 --- a/src/DependencyInjection/HttplugExtension.php +++ b/src/DependencyInjection/HttplugExtension.php @@ -12,6 +12,8 @@ use Http\Client\Common\PluginClientFactory; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient; +use Http\Client\Plugin\Vcr\RecordPlugin; +use Http\Client\Plugin\Vcr\ReplayPlugin; use Http\Message\Authentication\BasicAuth; use Http\Message\Authentication\Bearer; use Http\Message\Authentication\QueryParam; @@ -35,6 +37,13 @@ */ class HttplugExtension extends Extension { + /** + * Used to check is the VCR plugin is installed. + * + * @var bool + */ + private $useVcrPlugin = false; + /** * {@inheritdoc} */ @@ -94,6 +103,14 @@ public function load(array $configs, ContainerBuilder $container) $container->removeAlias(HttpAsyncClient::class); $container->removeAlias(HttpClient::class); } + + if ($this->useVcrPlugin) { + if (!\class_exists(RecordPlugin::class)) { + throw new \Exception('You need to require the VCR plugin to be able to use it: "composer require --dev php-http/vcr-plugin".'); + } + + $loader->load('vcr-plugin.xml'); + } } /** @@ -359,12 +376,20 @@ private function configureClient(ContainerBuilder $container, $clientName, array foreach ($arguments['plugins'] as $plugin) { $pluginName = key($plugin); $pluginConfig = current($plugin); - if ('reference' === $pluginName) { - $plugins[] = $pluginConfig['id']; - } elseif ('authentication' === $pluginName) { - $plugins = array_merge($plugins, $this->configureAuthentication($container, $pluginConfig, $serviceId.'.authentication')); - } else { - $plugins[] = $this->configurePlugin($container, $serviceId, $pluginName, $pluginConfig); + + switch ($pluginName) { + case 'reference': + $plugins[] = $pluginConfig['id']; + break; + case 'authentication': + $plugins = array_merge($plugins, $this->configureAuthentication($container, $pluginConfig, $serviceId.'.authentication')); + break; + case 'vcr': + $this->useVcrPlugin = true; + $plugins = array_merge($plugins, $this->configureVcrPlugin($container, $pluginConfig, $serviceId.'.vcr')); + break; + default: + $plugins[] = $this->configurePlugin($container, $serviceId, $pluginName, $pluginConfig); } } @@ -508,13 +533,81 @@ private function configurePlugin(ContainerBuilder $container, $serviceId, $plugi { $pluginServiceId = $serviceId.'.plugin.'.$pluginName; - $definition = class_exists(ChildDefinition::class) - ? new ChildDefinition('httplug.plugin.'.$pluginName) - : new DefinitionDecorator('httplug.plugin.'.$pluginName); + $definition = $this->createChildDefinition('httplug.plugin.'.$pluginName); $this->configurePluginByName($pluginName, $definition, $pluginConfig, $container, $pluginServiceId); $container->setDefinition($pluginServiceId, $definition); return $pluginServiceId; } + + private function configureVcrPlugin(ContainerBuilder $container, array $config, $prefix) + { + $recorder = $config['recorder']; + $recorderId = in_array($recorder, ['filesystem', 'in_memory']) ? 'httplug.plugin.vcr.recorder.'.$recorder : $recorder; + $namingStrategyId = $config['naming_strategy']; + $replayId = $prefix.'.replay'; + $recordId = $prefix.'.record'; + + if ('filesystem' === $recorder) { + $recorderDefinition = $this->createChildDefinition('httplug.plugin.vcr.recorder.filesystem'); + $recorderDefinition->replaceArgument(0, $config['fixtures_directory']); + $recorderId = $prefix.'.recorder'; + + $container->setDefinition($recorderId, $recorderDefinition); + } + + if ('default' === $config['naming_strategy']) { + $namingStrategyId = $prefix.'.naming_strategy'; + $namingStrategy = $this->createChildDefinition('httplug.plugin.vcr.naming_strategy.path'); + + if (!empty($config['naming_strategy_options'])) { + $namingStrategy->setArguments([$config['naming_strategy_options']]); + } + + $container->setDefinition($namingStrategyId, $namingStrategy); + } + + $arguments = [ + new Reference($namingStrategyId), + new Reference($recorderId), + ]; + $record = new Definition(RecordPlugin::class, $arguments); + $replay = new Definition(ReplayPlugin::class, $arguments); + $plugins = []; + + switch ($config['mode']) { + case 'replay': + $container->setDefinition($replayId, $replay); + $plugins[] = $replayId; + break; + case 'replay_or_record': + $replay->setArgument(2, false); + $container->setDefinition($replayId, $replay); + $container->setDefinition($recordId, $record); + $plugins[] = $replayId; + $plugins[] = $recordId; + break; + case 'record': + $container->setDefinition($recordId, $record); + $plugins[] = $recordId; + break; + } + + return $plugins; + } + + /** + * BC for old Symfony versions. Remove this method and use new ChildDefinition directly when we drop support for Symfony 2. + * + * @param string $parent the parent service id + * + * @return ChildDefinition|DefinitionDecorator + */ + private function createChildDefinition($parent) + { + $definitionClass = class_exists(ChildDefinition::class) ? ChildDefinition::class : DefinitionDecorator::class; + + return new $definitionClass($parent); + } } diff --git a/src/Resources/config/vcr-plugin.xml b/src/Resources/config/vcr-plugin.xml new file mode 100644 index 00000000..68c8bd69 --- /dev/null +++ b/src/Resources/config/vcr-plugin.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Resources/Fixtures/config/full.php b/tests/Resources/Fixtures/config/full.php index 399b1d25..8c9def8a 100644 --- a/tests/Resources/Fixtures/config/full.php +++ b/tests/Resources/Fixtures/config/full.php @@ -62,7 +62,7 @@ 'password' => 'bar', ], ], - ] + ], ], ], ], diff --git a/tests/Unit/DependencyInjection/HttplugExtensionTest.php b/tests/Unit/DependencyInjection/HttplugExtensionTest.php index 0750f5ca..397f813a 100644 --- a/tests/Unit/DependencyInjection/HttplugExtensionTest.php +++ b/tests/Unit/DependencyInjection/HttplugExtensionTest.php @@ -3,6 +3,7 @@ namespace Http\HttplugBundle\Tests\Unit\DependencyInjection; use Http\Client\HttpClient; +use Http\Client\Plugin\Vcr\Recorder\InMemoryRecorder; use Http\HttplugBundle\Collector\PluginClientFactoryListener; use Http\HttplugBundle\DependencyInjection\HttplugExtension; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; @@ -449,4 +450,67 @@ public function testBatchClientCanBePublic() $this->assertFalse($this->container->getDefinition('httplug.client.acme.batch_client')->isPrivate()); } } + + /** + * @dataProvider provideVcrPluginConfig + * @group vcr-plugin + */ + public function testVcrPluginConfiguration(array $config, array $services, array $arguments = []) + { + $prefix = 'httplug.client.acme.vcr'; + $this->load(['clients' => ['acme' => ['plugins' => [['vcr' => $config]]]]]); + $this->assertContainerBuilderHasService('httplug.plugin.vcr.recorder.in_memory', InMemoryRecorder::class); + + foreach ($services as $service) { + $this->assertContainerBuilderHasService($prefix.'.'.$service); + } + + foreach ($arguments as $id => $args) { + foreach ($args as $index => $value) { + $this->assertContainerBuilderHasServiceDefinitionWithArgument($prefix.'.'.$id, $index, $value); + } + } + } + + /** + * @group vcr-plugin + */ + public function testIsNotLoadedUnlessNeeded() + { + $this->load(['clients' => ['acme' => ['plugins' => []]]]); + $this->assertContainerBuilderNotHasService('httplug.plugin.vcr.recorder.in_memory'); + } + + public function provideVcrPluginConfig() + { + $config = [ + 'mode' => 'record', + 'recorder' => 'in_memory', + 'naming_strategy' => 'app.naming_strategy', + ]; + yield [$config, ['record']]; + + $config['mode'] = 'replay'; + yield [$config, ['replay']]; + + $config['mode'] = 'replay_or_record'; + yield [$config, ['replay', 'record']]; + + $config['recorder'] = 'filesystem'; + $config['fixtures_directory'] = __DIR__; + unset($config['naming_strategy']); + + yield [$config, ['replay', 'record', 'recorder', 'naming_strategy'], ['replay' => [2 => false]]]; + + $config['naming_strategy_options'] = [ + 'hash_headers' => ['X-FOO'], + 'hash_body_methods' => ['PATCH'], + ]; + + yield [ + $config, + ['replay', 'record', 'recorder', 'naming_strategy'], + ['naming_strategy' => [$config['naming_strategy_options']]], + ]; + } }