-
Notifications
You must be signed in to change notification settings - Fork 4
[Plugin] Add VCR Record & Replay plugins #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
61683f5
fe64879
5cd1243
42412b7
3f0af84
9939eff
b4fc311
76fab38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Http\Client\Plugin\Vcr\NamingStrategy; | ||
|
||
use Psr\Http\Message\RequestInterface; | ||
|
||
/** | ||
* Provides a deterministic and unique identifier for a request. The identifier must be safe to use with a filesystem. | ||
* | ||
* @author Gary PEGEOT <garypegeot@gmail.com> | ||
*/ | ||
interface NamingStrategyInterface | ||
{ | ||
public function name(RequestInterface $request): string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Http\Client\Plugin\Vcr\NamingStrategy; | ||
|
||
use Psr\Http\Message\RequestInterface; | ||
use Symfony\Component\OptionsResolver\Options; | ||
use Symfony\Component\OptionsResolver\OptionsResolver; | ||
|
||
/** | ||
* Will use the request attributes (hostname, path, headers & body) as filename. | ||
* | ||
* @author Gary PEGEOT <garypegeot@gmail.com> | ||
*/ | ||
class PathNamingStrategy implements NamingStrategyInterface | ||
{ | ||
/** | ||
* @var array | ||
*/ | ||
private $options; | ||
|
||
/** | ||
* @param array $options available options: | ||
* - hash_headers: the list of header names to hash, | ||
* - hash_body_methods: Methods for which the body will be hashed (Default: PUT, POST, PATCH) | ||
*/ | ||
public function __construct(array $options = []) | ||
{ | ||
$resolver = new OptionsResolver(); | ||
$this->configureOptions($resolver); | ||
$this->options = $resolver->resolve($options); | ||
} | ||
|
||
public function name(RequestInterface $request): string | ||
{ | ||
$parts = [$request->getUri()->getHost()]; | ||
|
||
$method = strtoupper($request->getMethod()); | ||
|
||
$parts[] = $method; | ||
$parts[] = str_replace('/', '_', trim($request->getUri()->getPath(), '/')); | ||
$parts[] = $this->getHeaderHash($request); | ||
|
||
if ($query = $request->getUri()->getQuery()) { | ||
$parts[] = $this->hash($query); | ||
} | ||
|
||
if (\in_array($method, $this->options['hash_body_methods'], true)) { | ||
$parts[] = $this->hash((string) $request->getBody()); | ||
} | ||
|
||
return implode('_', array_filter($parts)); | ||
} | ||
|
||
private function configureOptions(OptionsResolver $resolver): void | ||
{ | ||
$resolver->setDefaults([ | ||
'hash_headers' => [], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i was wondering if we should add defaults here (COOKIE, AUTHORIZATION, ACCEPT, ACCEPT-ENCODING, ACCEPT-LANGUAGE) but i guess most of the time you don't care, and when you do you should explicitly configure the headers you need. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think configuration it's up to the user too |
||
'hash_body_methods' => ['PUT', 'POST', 'PATCH'], | ||
]); | ||
|
||
$resolver->setAllowedTypes('hash_headers', 'string[]'); | ||
$resolver->setAllowedTypes('hash_body_methods', 'string[]'); | ||
|
||
$normalizer = function (Options $options, $value) { | ||
return \is_array($value) ? array_map('strtoupper', $value) : $value; | ||
}; | ||
$resolver->setNormalizer('hash_headers', $normalizer); | ||
$resolver->setNormalizer('hash_body_methods', $normalizer); | ||
} | ||
|
||
private function hash(string $value): string | ||
{ | ||
return substr(sha1($value), 0, 5); | ||
} | ||
|
||
private function getHeaderHash(RequestInterface $request): ?string | ||
{ | ||
$headers = []; | ||
|
||
foreach ($this->options['hash_headers'] as $name) { | ||
if ($request->hasHeader($name)) { | ||
$headers[] = "$name:".implode(',', $request->getHeader($name)); | ||
} | ||
} | ||
|
||
return empty($headers) ? null : $this->hash(implode(';', $headers)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Http\Client\Plugin\Vcr; | ||
|
||
use Http\Client\Common\Plugin; | ||
use Http\Client\Plugin\Vcr\NamingStrategy\NamingStrategyInterface; | ||
use Http\Client\Plugin\Vcr\Recorder\RecorderInterface; | ||
use Http\Promise\Promise; | ||
use Psr\Http\Message\RequestInterface; | ||
use Psr\Http\Message\ResponseInterface; | ||
|
||
final class RecordPlugin implements Plugin | ||
{ | ||
const HEADER_NAME = 'X-VCR-RECORD'; | ||
|
||
/** | ||
* @var NamingStrategyInterface | ||
*/ | ||
private $namingStrategy; | ||
|
||
/** | ||
* @var RecorderInterface | ||
*/ | ||
private $recorder; | ||
|
||
public function __construct(NamingStrategyInterface $namingStrategy, RecorderInterface $recorder) | ||
{ | ||
$this->namingStrategy = $namingStrategy; | ||
$this->recorder = $recorder; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise | ||
{ | ||
$name = $this->namingStrategy->name($request); | ||
|
||
return $next($request)->then(function (ResponseInterface $response) use ($name) { | ||
if (!$response->hasHeader(ReplayPlugin::HEADER_NAME)) { | ||
$this->recorder->record($name, $response); | ||
$response = $response->withAddedHeader(static::HEADER_NAME, $name); | ||
} | ||
|
||
return $response; | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Http\Client\Plugin\Vcr\Recorder; | ||
|
||
use GuzzleHttp\Psr7; | ||
use Psr\Http\Message\ResponseInterface; | ||
use Psr\Log\LoggerAwareInterface; | ||
use Psr\Log\LoggerAwareTrait; | ||
use Symfony\Component\Filesystem\Exception\IOException; | ||
use Symfony\Component\Filesystem\Filesystem; | ||
|
||
/** | ||
* Stores responses using the `guzzlehttp/psr7` library to serialize and deserialize the response. | ||
* Target directory should be part of your VCS. | ||
* | ||
dbu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* @author Gary PEGEOT <garypegeot@gmail.com> | ||
*/ | ||
final class FilesystemRecorder implements RecorderInterface, PlayerInterface, LoggerAwareInterface | ||
{ | ||
use LoggerAwareTrait; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $directory; | ||
|
||
/** | ||
* @var Filesystem | ||
*/ | ||
private $filesystem; | ||
|
||
public function __construct(string $directory, ?Filesystem $filesystem = null) | ||
{ | ||
$this->filesystem = $filesystem ?? new Filesystem(); | ||
|
||
if (!$this->filesystem->exists($directory)) { | ||
try { | ||
$this->filesystem->mkdir($directory); | ||
} catch (IOException $e) { | ||
throw new \InvalidArgumentException("Unable to create directory \"$directory\"/: {$e->getMessage()}", $e->getCode(), $e); | ||
} | ||
} | ||
|
||
$this->directory = realpath($directory).\DIRECTORY_SEPARATOR; | ||
} | ||
|
||
public function replay(string $name): ?ResponseInterface | ||
{ | ||
$filename = "{$this->directory}$name.txt"; | ||
$context = compact('filename'); | ||
|
||
if (!$this->filesystem->exists($filename)) { | ||
$this->log('Unable to replay {filename}', $context); | ||
|
||
return null; | ||
} | ||
|
||
$this->log('Response replayed from {filename}', $context); | ||
|
||
return Psr7\parse_response(file_get_contents($filename)); | ||
} | ||
|
||
public function record(string $name, ResponseInterface $response): void | ||
{ | ||
$filename = "{$this->directory}$name.txt"; | ||
$context = compact('name', 'filename'); | ||
|
||
$this->filesystem->dumpFile($filename, Psr7\str($response)); | ||
|
||
$this->log('Response for {name} stored into {filename}', $context); | ||
} | ||
|
||
private function log(string $message, array $context = []): void | ||
{ | ||
if ($this->logger) { | ||
$this->logger->debug("[VCR-PLUGIN][FilesystemRecorder] $message", $context); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.