Skip to content

Commit

Permalink
feat(php): Implement RawClient.php (#4625)
Browse files Browse the repository at this point in the history
  • Loading branch information
amckinney authored Sep 12, 2024
1 parent 233c0d9 commit 39e8f85
Show file tree
Hide file tree
Showing 442 changed files with 17,553 additions and 1,428 deletions.
4 changes: 4 additions & 0 deletions generators/php/codegen/src/AsIs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
export enum AsIsFiles {
BaseApiRequest = "BaseApiRequest.Template.php",
ClientOptions = "ClientOptions.Template.php",
GitIgnore = ".gitignore",
GithubCiYml = "github-ci.yml",
HttpMethod = "HttpMethod.Template.php",
JsonApiRequest = "JsonApiRequest.Template.php",
PhpStanNeon = "phpstan.neon",
PhpUnitXml = "phpunit.xml",
RawClient = "RawClient.Template.php",
Expand Down
22 changes: 22 additions & 0 deletions generators/php/codegen/src/asIs/BaseApiRequest.Template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace <%= namespace%>;

abstract class BaseApiRequest
{
/**
* @param string $baseUrl The base URL for the request
* @param string $path The path for the request
* @param HttpMethod $method The HTTP method for the request
* @param array<string, string> $headers Additional headers for the request (optional)
* @param array<string, mixed> $query Query parameters for the request (optional)
*/
public function __construct(
public readonly string $baseUrl,
public readonly string $path,
public readonly HttpMethod $method,
public readonly array $headers = [],
public readonly array $query = [],
) {
}
}
12 changes: 12 additions & 0 deletions generators/php/codegen/src/asIs/HttpMethod.Template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace <%= namespace%>;

enum HttpMethod
{
case GET;
case POST;
case PUT;
case PATCH;
case DELETE;
}
25 changes: 25 additions & 0 deletions generators/php/codegen/src/asIs/JsonApiRequest.Template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace <%= namespace%>;

class JsonApiRequest extends BaseApiRequest
{
/**
* @param string $baseUrl The base URL for the request
* @param string $path The path for the request
* @param HttpMethod $method The HTTP method for the request
* @param array<string, string> $headers Additional headers for the request (optional)
* @param array<string, mixed> $query Query parameters for the request (optional)
* @param mixed|null $body The JSON request body (optional)
*/
public function __construct(
string $baseUrl,
string $path,
HttpMethod $method,
array $headers = [],
array $query = [],
public readonly mixed $body = null
) {
parent::__construct($baseUrl, $path, $method, $headers, $query);
}
}
118 changes: 115 additions & 3 deletions generators/php/codegen/src/asIs/RawClient.Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,122 @@

namespace <%= namespace%>;

use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Utils;
use InvalidArgumentException;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;

class RawClient
{
public function __construct()
{
// TODO: Implement me!
/**
* @param ClientInterface $client The HTTP client used to make requests.
* @param array<string, string> $headers The HTTP headers sent with the request.
*/
public function __construct(
private readonly ClientInterface $client,
private readonly array $headers = [],
) {
}

/**
* @throws ClientExceptionInterface
*/
public function sendRequest(
BaseApiRequest $request,
): ResponseInterface {
$httpRequest = $this->buildRequest($request);
return $this->client->send($httpRequest);
}

private function buildRequest(
BaseApiRequest $request
): Request {
$url = $this->buildUrl($request);
$headers = $this->encodeHeaders($request);
$body = $this->encodeRequestBody($request);
return new Request(
$request->method->name,
$url,
$headers,
$body,
);
}

/**
* @return array<string, string>
*/
private function encodeHeaders(
BaseApiRequest $request
): array {
return match (get_class($request)) {
JsonApiRequest::class => array_merge(
["Content-Type" => "application/json"],
$this->headers,
$request->headers
),
default => throw new InvalidArgumentException('Unsupported request type: ' . get_class($request)),
};
}

private function encodeRequestBody(
BaseApiRequest $request
): ?StreamInterface {
return match (get_class($request)) {
JsonApiRequest::class => $request->body != null ? Utils::streamFor(json_encode($request->body)) : null,
default => throw new InvalidArgumentException('Unsupported request type: '.get_class($request)),
};
}

private function buildUrl(
BaseApiRequest $request
): string {
$baseUrl = $request->baseUrl;
$trimmedBaseUrl = rtrim($baseUrl, '/');
$trimmedBasePath = ltrim($request->path, '/');
$url = "{$trimmedBaseUrl}/{$trimmedBasePath}";

if (!empty($request->query)) {
$url .= '?' . $this->encodeQuery($request->query);
}

return $url;
}

/**
* @param array<string, mixed> $query
*/
private function encodeQuery(
array $query
): string {
$parts = [];
foreach ($query as $key => $value) {
if (is_array($value)) {
foreach ($value as $item) {
$parts[] = urlencode($key).'='.$this->encodeQueryValue($item);
}
} else {
$parts[] = urlencode($key).'='.$this->encodeQueryValue($value);
}
}
return implode('&', $parts);
}

private function encodeQueryValue(
mixed $value
): string {
if (is_string($value)) {
return urlencode($value);
}
if (is_scalar($value)) {
return urlencode((string)$value);
}
if (is_null($value)) {
return 'null';
}
// Unreachable, but included for a best effort.
return urlencode(strval(json_encode($value)));
}
}
93 changes: 91 additions & 2 deletions generators/php/codegen/src/asIs/RawClientTest.Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,101 @@

namespace <%= namespace%>;

use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use <%= coreNamespace%>\BaseApiRequest;
use <%= coreNamespace%>\JsonApiRequest;
use <%= coreNamespace%>\HttpMethod;
use <%= coreNamespace%>\RawClient;


class RawClientTest extends TestCase
{
public function testRawClient()
private string $baseUrl = 'https://api.example.com';
private MockHandler $mockHandler;
private RawClient $rawClient;

protected function setUp(): void
{
$this->mockHandler = new MockHandler();
$handlerStack = HandlerStack::create($this->mockHandler);
$client = new Client(['handler' => $handlerStack]);
$this->rawClient = new RawClient($client);
}

public function testHeaders(): void
{
$this->mockHandler->append(new Response(200));

$request = new JsonApiRequest(
$this->baseUrl,
'/test',
HttpMethod::GET,
['X-Custom-Header' => 'TestValue']
);

$this->sendRequest($request);

$lastRequest = $this->mockHandler->getLastRequest();
assert($lastRequest instanceof RequestInterface);
$this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type'));
$this->assertEquals('TestValue', $lastRequest->getHeaderLine('X-Custom-Header'));
}

public function testQueryParameters(): void
{
$this->mockHandler->append(new Response(200));

$request = new JsonApiRequest(
$this->baseUrl,
'/test',
HttpMethod::GET,
[],
['param1' => 'value1', 'param2' => ['a', 'b'], 'param3' => 'true']
);

$this->sendRequest($request);

$lastRequest = $this->mockHandler->getLastRequest();
assert($lastRequest instanceof RequestInterface);
$this->assertEquals(
'https://api.example.com/test?param1=value1&param2=a&param2=b&param3=true',
(string)$lastRequest->getUri()
);
}

public function testJsonBody(): void
{
$this->mockHandler->append(new Response(200));

$body = ['key' => 'value'];
$request = new JsonApiRequest(
$this->baseUrl,
'/test',
HttpMethod::POST,
[],
[],
$body
);

$this->sendRequest($request);

$lastRequest = $this->mockHandler->getLastRequest();
assert($lastRequest instanceof RequestInterface);
$this->assertEquals('application/json', $lastRequest->getHeaderLine('Content-Type'));
$this->assertEquals(json_encode($body), (string)$lastRequest->getBody());
}

private function sendRequest(BaseApiRequest $request): void
{
$this->assertTrue(true);
try {
$this->rawClient->sendRequest($request);
} catch (\Throwable $e) {
$this->fail('An exception was thrown: ' . $e->getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export abstract class AbstractPhpGeneratorContext<
}

public getCoreTestsNamespace(): string {
return `${this.namespace}\\Core\\Tests`;
return `${this.namespace}\\Tests\\Core`;
}

public abstract getRawAsIsFiles(): string[];
Expand Down
Loading

0 comments on commit 39e8f85

Please sign in to comment.