Skip to content

Commit bad3276

Browse files
GaryPEGEOTdbu
authored andcommitted
Add integration for VCR plugin (#333)
1 parent dd63ce2 commit bad3276

File tree

7 files changed

+242
-11
lines changed

7 files changed

+242
-11
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release.
44

5+
### Added
6+
7+
- Integration for VCR Plugin
8+
59
## 1.15.2 - 2019-04-18
610

711
### Fixed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@
3838
"nyholm/nsa": "^1.1",
3939
"php-http/cache-plugin": "^1.6",
4040
"php-http/guzzle6-adapter": "^1.1.1 || ^2.0.1",
41-
"php-http/promise": "^1.0",
4241
"php-http/mock-client": "^1.2",
42+
"php-http/promise": "^1.0",
43+
"php-http/vcr-plugin": "^1.0@dev",
4344
"polishsymfonycommunity/symfony-mocker-container": "^1.0",
4445
"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",
4546
"symfony/cache": "^3.1.10 || ^3.2.14 || ^3.3.18 || ^3.4.20 || ^4.0.15 || ^4.1.9 || ^4.2.1",

src/DependencyInjection/Configuration.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
use Http\Client\Common\Plugin\Cache\Generator\CacheKeyGenerator;
66
use Http\Client\Common\Plugin\CachePlugin;
77
use Http\Client\Common\Plugin\Journal;
8+
use Http\Client\Plugin\Vcr\NamingStrategy\NamingStrategyInterface;
9+
use Http\Client\Plugin\Vcr\Recorder\PlayerInterface;
10+
use Http\Client\Plugin\Vcr\Recorder\RecorderInterface;
811
use Http\Message\CookieJar;
912
use Http\Message\Formatter;
1013
use Http\Message\StreamFactory;
@@ -437,6 +440,50 @@ private function createClientPluginNode()
437440
->end()
438441
->end()
439442
->end()
443+
->arrayNode('vcr')
444+
->canBeEnabled()
445+
->addDefaultsIfNotSet()
446+
->info('Record response to be replayed during tests or development cycle.')
447+
->validate()
448+
->ifTrue(function ($config) {
449+
return 'filesystem' === $config['recorder'] && empty($config['fixtures_directory']);
450+
})
451+
->thenInvalid('If you want to use the "filesystem" recorder you must also specify a "fixtures_directory".')
452+
->end()
453+
->children()
454+
->enumNode('mode')
455+
->info('What should be the behavior of the plugin?')
456+
->values(['record', 'replay', 'replay_or_record'])
457+
->isRequired()
458+
->cannotBeEmpty()
459+
->end()
460+
->scalarNode('recorder')
461+
->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))
462+
->defaultValue('filesystem')
463+
->cannotBeEmpty()
464+
->end()
465+
->scalarNode('naming_strategy')
466+
->info(sprintf('Which naming strategy to use. Add the ID of your service implementing %s to override the default one.', NamingStrategyInterface::class))
467+
->defaultValue('default')
468+
->cannotBeEmpty()
469+
->end()
470+
->arrayNode('naming_strategy_options')
471+
->info('See http://docs.php-http.org/en/latest/plugins/vcr.html#the-naming-strategy for more details')
472+
->children()
473+
->arrayNode('hash_headers')
474+
->info('List of header(s) that make the request unique (Ex: ‘Authorization’)')
475+
->prototype('scalar')->end()
476+
->end()
477+
->arrayNode('hash_body_methods')
478+
->info('for which request methods the body makes requests distinct.')
479+
->prototype('scalar')->end()
480+
->end()
481+
->end()
482+
->end() // End naming_strategy_options
483+
->scalarNode('fixtures_directory')
484+
->info('Where the responses will be stored and replay from when using the filesystem recorder. Should be accessible to your VCS.')
485+
->end()
486+
->end()
440487
->end()
441488
->end();
442489

src/DependencyInjection/HttplugExtension.php

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use Http\Client\Common\PluginClientFactory;
1313
use Http\Client\HttpAsyncClient;
1414
use Http\Client\HttpClient;
15+
use Http\Client\Plugin\Vcr\RecordPlugin;
16+
use Http\Client\Plugin\Vcr\ReplayPlugin;
1517
use Http\Message\Authentication\BasicAuth;
1618
use Http\Message\Authentication\Bearer;
1719
use Http\Message\Authentication\QueryParam;
@@ -35,6 +37,13 @@
3537
*/
3638
class HttplugExtension extends Extension
3739
{
40+
/**
41+
* Used to check is the VCR plugin is installed.
42+
*
43+
* @var bool
44+
*/
45+
private $useVcrPlugin = false;
46+
3847
/**
3948
* {@inheritdoc}
4049
*/
@@ -94,6 +103,14 @@ public function load(array $configs, ContainerBuilder $container)
94103
$container->removeAlias(HttpAsyncClient::class);
95104
$container->removeAlias(HttpClient::class);
96105
}
106+
107+
if ($this->useVcrPlugin) {
108+
if (!\class_exists(RecordPlugin::class)) {
109+
throw new \Exception('You need to require the VCR plugin to be able to use it: "composer require --dev php-http/vcr-plugin".');
110+
}
111+
112+
$loader->load('vcr-plugin.xml');
113+
}
97114
}
98115

99116
/**
@@ -359,12 +376,20 @@ private function configureClient(ContainerBuilder $container, $clientName, array
359376
foreach ($arguments['plugins'] as $plugin) {
360377
$pluginName = key($plugin);
361378
$pluginConfig = current($plugin);
362-
if ('reference' === $pluginName) {
363-
$plugins[] = $pluginConfig['id'];
364-
} elseif ('authentication' === $pluginName) {
365-
$plugins = array_merge($plugins, $this->configureAuthentication($container, $pluginConfig, $serviceId.'.authentication'));
366-
} else {
367-
$plugins[] = $this->configurePlugin($container, $serviceId, $pluginName, $pluginConfig);
379+
380+
switch ($pluginName) {
381+
case 'reference':
382+
$plugins[] = $pluginConfig['id'];
383+
break;
384+
case 'authentication':
385+
$plugins = array_merge($plugins, $this->configureAuthentication($container, $pluginConfig, $serviceId.'.authentication'));
386+
break;
387+
case 'vcr':
388+
$this->useVcrPlugin = true;
389+
$plugins = array_merge($plugins, $this->configureVcrPlugin($container, $pluginConfig, $serviceId.'.vcr'));
390+
break;
391+
default:
392+
$plugins[] = $this->configurePlugin($container, $serviceId, $pluginName, $pluginConfig);
368393
}
369394
}
370395

@@ -508,13 +533,81 @@ private function configurePlugin(ContainerBuilder $container, $serviceId, $plugi
508533
{
509534
$pluginServiceId = $serviceId.'.plugin.'.$pluginName;
510535

511-
$definition = class_exists(ChildDefinition::class)
512-
? new ChildDefinition('httplug.plugin.'.$pluginName)
513-
: new DefinitionDecorator('httplug.plugin.'.$pluginName);
536+
$definition = $this->createChildDefinition('httplug.plugin.'.$pluginName);
514537

515538
$this->configurePluginByName($pluginName, $definition, $pluginConfig, $container, $pluginServiceId);
516539
$container->setDefinition($pluginServiceId, $definition);
517540

518541
return $pluginServiceId;
519542
}
543+
544+
private function configureVcrPlugin(ContainerBuilder $container, array $config, $prefix)
545+
{
546+
$recorder = $config['recorder'];
547+
$recorderId = in_array($recorder, ['filesystem', 'in_memory']) ? 'httplug.plugin.vcr.recorder.'.$recorder : $recorder;
548+
$namingStrategyId = $config['naming_strategy'];
549+
$replayId = $prefix.'.replay';
550+
$recordId = $prefix.'.record';
551+
552+
if ('filesystem' === $recorder) {
553+
$recorderDefinition = $this->createChildDefinition('httplug.plugin.vcr.recorder.filesystem');
554+
$recorderDefinition->replaceArgument(0, $config['fixtures_directory']);
555+
$recorderId = $prefix.'.recorder';
556+
557+
$container->setDefinition($recorderId, $recorderDefinition);
558+
}
559+
560+
if ('default' === $config['naming_strategy']) {
561+
$namingStrategyId = $prefix.'.naming_strategy';
562+
$namingStrategy = $this->createChildDefinition('httplug.plugin.vcr.naming_strategy.path');
563+
564+
if (!empty($config['naming_strategy_options'])) {
565+
$namingStrategy->setArguments([$config['naming_strategy_options']]);
566+
}
567+
568+
$container->setDefinition($namingStrategyId, $namingStrategy);
569+
}
570+
571+
$arguments = [
572+
new Reference($namingStrategyId),
573+
new Reference($recorderId),
574+
];
575+
$record = new Definition(RecordPlugin::class, $arguments);
576+
$replay = new Definition(ReplayPlugin::class, $arguments);
577+
$plugins = [];
578+
579+
switch ($config['mode']) {
580+
case 'replay':
581+
$container->setDefinition($replayId, $replay);
582+
$plugins[] = $replayId;
583+
break;
584+
case 'replay_or_record':
585+
$replay->setArgument(2, false);
586+
$container->setDefinition($replayId, $replay);
587+
$container->setDefinition($recordId, $record);
588+
$plugins[] = $replayId;
589+
$plugins[] = $recordId;
590+
break;
591+
case 'record':
592+
$container->setDefinition($recordId, $record);
593+
$plugins[] = $recordId;
594+
break;
595+
}
596+
597+
return $plugins;
598+
}
599+
600+
/**
601+
* BC for old Symfony versions. Remove this method and use new ChildDefinition directly when we drop support for Symfony 2.
602+
*
603+
* @param string $parent the parent service id
604+
*
605+
* @return ChildDefinition|DefinitionDecorator
606+
*/
607+
private function createChildDefinition($parent)
608+
{
609+
$definitionClass = class_exists(ChildDefinition::class) ? ChildDefinition::class : DefinitionDecorator::class;
610+
611+
return new $definitionClass($parent);
612+
}
520613
}

src/Resources/config/vcr-plugin.xml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<container xmlns="http://symfony.com/schema/dic/services"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
5+
6+
<services>
7+
<!-- Recorders -->
8+
<service id="httplug.plugin.vcr.recorder.filesystem" class="Http\Client\Plugin\Vcr\Recorder\FilesystemRecorder" public="false" abstract="true">
9+
<argument/>
10+
<argument type="service" id="filesystem" on-invalid="null"/>
11+
<call method="setLogger">
12+
<argument type="service" id="logger" on-invalid="null"/>
13+
</call>
14+
</service>
15+
<service id="httplug.plugin.vcr.recorder.in_memory" class="Http\Client\Plugin\Vcr\Recorder\InMemoryRecorder" public="false"/>
16+
<!-- /Recorders -->
17+
18+
<!-- Naming strategies -->
19+
<service id="httplug.plugin.vcr.naming_strategy.path" class="Http\Client\Plugin\Vcr\NamingStrategy\PathNamingStrategy" abstract="true" public="false"/>
20+
<!-- /Naming strategies -->
21+
</services>
22+
</container>

tests/Resources/Fixtures/config/full.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
'password' => 'bar',
6363
],
6464
],
65-
]
65+
],
6666
],
6767
],
6868
],

tests/Unit/DependencyInjection/HttplugExtensionTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Http\HttplugBundle\Tests\Unit\DependencyInjection;
44

55
use Http\Client\HttpClient;
6+
use Http\Client\Plugin\Vcr\Recorder\InMemoryRecorder;
67
use Http\HttplugBundle\Collector\PluginClientFactoryListener;
78
use Http\HttplugBundle\DependencyInjection\HttplugExtension;
89
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase;
@@ -449,4 +450,67 @@ public function testBatchClientCanBePublic()
449450
$this->assertFalse($this->container->getDefinition('httplug.client.acme.batch_client')->isPrivate());
450451
}
451452
}
453+
454+
/**
455+
* @dataProvider provideVcrPluginConfig
456+
* @group vcr-plugin
457+
*/
458+
public function testVcrPluginConfiguration(array $config, array $services, array $arguments = [])
459+
{
460+
$prefix = 'httplug.client.acme.vcr';
461+
$this->load(['clients' => ['acme' => ['plugins' => [['vcr' => $config]]]]]);
462+
$this->assertContainerBuilderHasService('httplug.plugin.vcr.recorder.in_memory', InMemoryRecorder::class);
463+
464+
foreach ($services as $service) {
465+
$this->assertContainerBuilderHasService($prefix.'.'.$service);
466+
}
467+
468+
foreach ($arguments as $id => $args) {
469+
foreach ($args as $index => $value) {
470+
$this->assertContainerBuilderHasServiceDefinitionWithArgument($prefix.'.'.$id, $index, $value);
471+
}
472+
}
473+
}
474+
475+
/**
476+
* @group vcr-plugin
477+
*/
478+
public function testIsNotLoadedUnlessNeeded()
479+
{
480+
$this->load(['clients' => ['acme' => ['plugins' => []]]]);
481+
$this->assertContainerBuilderNotHasService('httplug.plugin.vcr.recorder.in_memory');
482+
}
483+
484+
public function provideVcrPluginConfig()
485+
{
486+
$config = [
487+
'mode' => 'record',
488+
'recorder' => 'in_memory',
489+
'naming_strategy' => 'app.naming_strategy',
490+
];
491+
yield [$config, ['record']];
492+
493+
$config['mode'] = 'replay';
494+
yield [$config, ['replay']];
495+
496+
$config['mode'] = 'replay_or_record';
497+
yield [$config, ['replay', 'record']];
498+
499+
$config['recorder'] = 'filesystem';
500+
$config['fixtures_directory'] = __DIR__;
501+
unset($config['naming_strategy']);
502+
503+
yield [$config, ['replay', 'record', 'recorder', 'naming_strategy'], ['replay' => [2 => false]]];
504+
505+
$config['naming_strategy_options'] = [
506+
'hash_headers' => ['X-FOO'],
507+
'hash_body_methods' => ['PATCH'],
508+
];
509+
510+
yield [
511+
$config,
512+
['replay', 'record', 'recorder', 'naming_strategy'],
513+
['naming_strategy' => [$config['naming_strategy_options']]],
514+
];
515+
}
452516
}

0 commit comments

Comments
 (0)