Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"jms/serializer": "^3.0",
"doctrine/annotations": "^2.0",
"guzzlehttp/psr7": "^2.0",
"psr/http-client": "^1.0",
"deviantintegral/jms-serializer-uri-handler": "^1.1",
"deviantintegral/null-date-time": "^1.0",
"symfony/console": "^7||^8"
Expand Down
113 changes: 113 additions & 0 deletions src/HarRecorder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types=1);

namespace Deviantintegral\Har;

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* A PSR-18 HTTP client decorator that records request/response traffic to HAR format.
*
* Use case: Generate test fixtures by recording real API interactions.
*
* Example usage:
* $recorder = new HarRecorder($actualHttpClient);
* $response = $recorder->sendRequest($request); // Makes real request
* $har = $recorder->getHar(); // Get recorded traffic
*/
final class HarRecorder implements ClientInterface
{
/**
* @var Entry[]
*/
private array $entries = [];

private Creator $creator;

/**
* @param ClientInterface $client The underlying HTTP client to delegate requests to
* @param string $creatorName Name of the application creating the HAR
* @param string $creatorVersion Version of the application
*/
public function __construct(
private readonly ClientInterface $client,
string $creatorName = 'deviantintegral/har',
string $creatorVersion = '1.0',
) {
$this->creator = (new Creator())
->setName($creatorName)
->setVersion($creatorVersion);
}

/**
* Send an HTTP request and record the request/response pair.
*
* @throws \Psr\Http\Client\ClientExceptionInterface
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
$startTime = hrtime(true);
$startDateTime = new \DateTime();

$response = $this->client->sendRequest($request);

$endTime = hrtime(true);
/** @infection-ignore-all Equivalent mutant: 1 part per million difference is not testable */
$totalTimeMs = ($endTime - $startTime) / 1_000_000;

$entry = $this->createEntry($request, $response, $startDateTime, $totalTimeMs);
$this->entries[] = $entry;

return $response;
}

/**
* Get the recorded traffic as a HAR object.
*/
public function getHar(): Har
{
$log = (new Log())
->setVersion('1.2')
->setCreator($this->creator)
->setEntries($this->entries);

return (new Har())->setLog($log);
}

/**
* Reset and clear all recorded entries.
*/
public function reset(): void
{
$this->entries = [];
}

/**
* Create a HAR entry from the request/response pair.
*/
private function createEntry(
RequestInterface $request,
ResponseInterface $response,
\DateTime $startDateTime,
float $totalTimeMs,
): Entry {
$harRequest = Request::fromPsr7Request($request);
$harResponse = Response::fromPsr7Response($response);

$timings = (new Timings())
->setSend(0)
->setWait($totalTimeMs)
->setReceive(0);

return (new Entry())
->setStartedDateTime($startDateTime)
->setTime($totalTimeMs)
->setRequest($harRequest)
->setResponse($harResponse)
->setCache(new Cache())
->setTimings($timings);
}
}
Loading