Skip to content

Commit

Permalink
HTTP client (#237)
Browse files Browse the repository at this point in the history
Wrap `file_get_contents($url)` or similar in `HttpClient` service so things using it can have tests written.
  • Loading branch information
spaze authored Sep 25, 2023
2 parents 4a83045 + 43bdd34 commit 8255311
Show file tree
Hide file tree
Showing 28 changed files with 781 additions and 266 deletions.
27 changes: 8 additions & 19 deletions site/app/CompanyInfo/CompanyRegisterAres.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

use MichalSpacekCz\CompanyInfo\Exceptions\CompanyInfoException;
use MichalSpacekCz\CompanyInfo\Exceptions\CompanyNotFoundException;
use MichalSpacekCz\Http\Client\HttpClient;
use MichalSpacekCz\Http\Client\HttpClientRequest;
use MichalSpacekCz\Http\Exceptions\HttpClientRequestException;
use Nette\Http\IResponse;
use Nette\Schema\Expect;
use Nette\Schema\Processor;
Expand All @@ -23,6 +26,7 @@ class CompanyRegisterAres implements CompanyRegister

public function __construct(
private readonly Processor $schemaProcessor,
private readonly HttpClient $httpClient,
) {
}

Expand Down Expand Up @@ -86,31 +90,16 @@ public function getDetails(string $companyId): CompanyInfoDetails


/**
* @throws CompanyInfoException
* @throws CompanyNotFoundException
*/
private function fetch(string $companyId): string
{
$url = "https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty/{$companyId}";
$context = stream_context_create();
$setResult = stream_context_set_params($context, [
'notification' => function (int $notificationCode, int $severity, ?string $message, int $messageCode) {
if ($severity === STREAM_NOTIFY_SEVERITY_ERR) {
throw new CompanyNotFoundException($messageCode !== IResponse::S404_NotFound ? $messageCode : null);
}
},
'options' => [
'http' => ['ignore_errors' => true], // To suppress PHP Warning: [...] HTTP/1.0 500 Internal Server Error
],
]);
if (!$setResult) {
throw new CompanyInfoException("Can't set stream context params to get contents from {$url}");
}
$result = file_get_contents($url, false, $context);
if (!$result) {
throw new CompanyInfoException("Can't get result from {$url}");
try {
return $this->httpClient->get(new HttpClientRequest($url))->getBody();
} catch (HttpClientRequestException $e) {
throw new CompanyNotFoundException($e->getCode() !== IResponse::S404_NotFound ? $e->getCode() : null, $e);
}
return $result;
}


Expand Down
17 changes: 13 additions & 4 deletions site/app/CompanyInfo/CompanyRegisterRegisterUz.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

use MichalSpacekCz\CompanyInfo\Exceptions\CompanyInfoException;
use MichalSpacekCz\CompanyInfo\Exceptions\CompanyNotFoundException;
use MichalSpacekCz\Http\Client\HttpClient;
use MichalSpacekCz\Http\Client\HttpClientRequest;
use MichalSpacekCz\Http\Exceptions\HttpClientRequestException;
use Nette\Http\IResponse;
use Nette\Utils\Json;
use Nette\Utils\JsonException;
Expand All @@ -23,6 +26,12 @@ class CompanyRegisterRegisterUz implements CompanyRegister
private const COUNTRY_CODE = 'sk';


public function __construct(
private readonly HttpClient $httpClient,
) {
}


public function getCountry(): string
{
return 'sk';
Expand Down Expand Up @@ -71,10 +80,10 @@ private function call(string $method, ?array $parameters = null): stdClass
} else {
$query = '';
}
$content = file_get_contents("https://www.registeruz.sk/cruz-public/api/{$method}{$query}");
if (!$content) {
$lastError = error_get_last();
throw new CompanyInfoException($lastError ? $lastError['message'] : '', IResponse::S500_InternalServerError);
try {
$content = $this->httpClient->get(new HttpClientRequest("https://www.registeruz.sk/cruz-public/api/{$method}{$query}"))->getBody();
} catch (HttpClientRequestException $e) {
throw new CompanyInfoException(code: IResponse::S500_InternalServerError, previous: $e);
}
try {
$data = Json::decode($content);
Expand Down
13 changes: 3 additions & 10 deletions site/app/Form/UpcKeysSsidFormFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

namespace MichalSpacekCz\Form;

use MichalSpacekCz\UpcKeys\Technicolor;
use MichalSpacekCz\UpcKeys\UpcKeys;

class UpcKeysSsidFormFactory
Expand All @@ -12,16 +11,14 @@ class UpcKeysSsidFormFactory
public function __construct(
private readonly UnprotectedFormFactory $factory,
private readonly UpcKeys $upcKeys,
private readonly Technicolor $technicolor,
) {
}


/**
* @param callable(string): void $onSuccess
* @param callable(): void $onError
*/
public function create(callable $onSuccess, callable $onError, ?string $ssid): UiForm
public function create(callable $onSuccess, ?string $ssid): UiForm
{
$form = $this->factory->create();
$form->addText('ssid', 'SSID:')
Expand All @@ -33,14 +30,10 @@ public function create(callable $onSuccess, callable $onError, ?string $ssid): U
$form->addSubmit('submit', 'Get keys')
->setHtmlId('submit')
->setHtmlAttribute('data-alt', 'Wait…');
$form->onSuccess[] = function (UiForm $form) use ($onSuccess, $onError): void {
$form->onSuccess[] = function (UiForm $form) use ($onSuccess): void {
$values = $form->getFormValues();
$ssid = strtoupper(trim($values->ssid));
if (!$this->technicolor->saveKeys($ssid)) {
$onError();
} else {
$onSuccess($ssid);
}
$onSuccess($ssid);
};
return $form;
}
Expand Down
113 changes: 113 additions & 0 deletions site/app/Http/Client/HttpClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Http\Client;

use MichalSpacekCz\Http\Exceptions\HttpClientRequestException;
use MichalSpacekCz\Http\Exceptions\HttpStreamException;

class HttpClient
{

/**
* @param array<string, string|int> $httpOptions
* @param array<string, string|bool> $tlsOptions
* @return resource
*/
private function createStreamContext(HttpClientRequest $request, array $httpOptions = [], array $tlsOptions = [])
{
$httpOptions = [
'ignore_errors' => true, // To suppress PHP Warning: [...] HTTP/1.0 500 Internal Server Error
'header' => $request->getHeaders(),
] + $httpOptions;
if ($request->getUserAgent() !== null) {
$httpOptions = ['user_agent' => str_replace('\\', '/', $request->getUserAgent())] + $httpOptions;
}
if ($request->getFollowLocation() !== null) {
$httpOptions = ['follow_location' => (int)$request->getFollowLocation()] + $httpOptions;
}
if ($request->getTlsCaptureCertificate() !== null) {
$tlsOptions = ['capture_peer_cert' => $request->getTlsCaptureCertificate()] + $tlsOptions;
}
if ($request->getTlsServerName() !== null) {
$tlsOptions = ['peer_name' => $request->getTlsServerName()] + $tlsOptions;
}
return stream_context_create(
[
'ssl' => $tlsOptions,
'http' => $httpOptions,
],
[
'notification' => function (int $notificationCode, int $severity, ?string $message, int $messageCode): void {
if ($severity === STREAM_NOTIFY_SEVERITY_ERR) {
throw new HttpStreamException($notificationCode, $message, $messageCode);
}
},
],
);
}


/**
* @throws HttpClientRequestException
*/
public function get(HttpClientRequest $request): HttpClientResponse
{
$context = $this->createStreamContext($request);
return $this->request($request, $context);
}


/**
* @throws HttpClientRequestException
*/
public function head(HttpClientRequest $request): HttpClientResponse
{
$context = $this->createStreamContext(
$request,
['method' => 'HEAD'],
);
return $this->request($request, $context);
}


/**
* @param array<string, string> $formData
* @throws HttpClientRequestException
*/
public function postForm(HttpClientRequest $request, array $formData = []): HttpClientResponse
{
$request->addHeader('Content-Type', 'application/x-www-form-urlencoded');
$context = $this->createStreamContext(
$request,
['method' => 'POST', 'content' => http_build_query($formData)],
);
return $this->request($request, $context);
}


/**
* @param resource $context
* @throws HttpClientRequestException
* @noinspection PhpRedundantCatchClauseInspection A notification callback created by self::createStreamContext() may throw HttpStreamException
*/
private function request(HttpClientRequest $request, $context): HttpClientResponse
{
try {
$fp = fopen($request->getUrl(), 'r', context: $context);
if (!$fp) {
throw new HttpClientRequestException($request->getUrl());
}
$result = stream_get_contents($fp);
$options = stream_context_get_options($fp);
fclose($fp);
} catch (HttpStreamException $e) {
throw new HttpClientRequestException($request->getUrl(), $e->getCode(), $e);
}
if ($result === false) {
throw new HttpClientRequestException($request->getUrl());
}
return new HttpClientResponse($request, $result, $options['ssl']['peer_certificate'] ?? null);
}

}
100 changes: 100 additions & 0 deletions site/app/Http/Client/HttpClientRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Http\Client;

final class HttpClientRequest
{

private ?string $userAgent = null;

/** @var list<string> */
private array $headers = [];

private ?bool $followLocation = null;

private ?string $tlsServerName = null;

private ?bool $tlsCaptureCertificate = null;


public function __construct(
private readonly string $url,
) {
}


public function getUrl(): string
{
return $this->url;
}


public function getUserAgent(): ?string
{
return $this->userAgent;
}


public function setUserAgent(string $userAgent): self
{
$this->userAgent = $userAgent;
return $this;
}


public function addHeader(string $header, string $value): self
{
$this->headers[] = "{$header}: {$value}";
return $this;
}


/**
* @return list<string>
*/
public function getHeaders(): array
{
return $this->headers;
}


public function getFollowLocation(): ?bool
{
return $this->followLocation;
}


public function setFollowLocation(bool $followLocation): self
{
$this->followLocation = $followLocation;
return $this;
}


public function getTlsServerName(): ?string
{
return $this->tlsServerName;
}


public function setTlsServerName(string $tlsServerName): self
{
$this->tlsServerName = $tlsServerName;
return $this;
}


public function getTlsCaptureCertificate(): ?bool
{
return $this->tlsCaptureCertificate;
}


public function setTlsCaptureCertificate(bool $tlsCaptureCertificate): self
{
$this->tlsCaptureCertificate = $tlsCaptureCertificate;
return $this;
}

}
43 changes: 43 additions & 0 deletions site/app/Http/Client/HttpClientResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
declare(strict_types = 1);

namespace MichalSpacekCz\Http\Client;

use MichalSpacekCz\Http\Exceptions\HttpClientTlsCertificateNotAvailableException;
use MichalSpacekCz\Http\Exceptions\HttpClientTlsCertificateNotCapturedException;
use OpenSSLCertificate;

class HttpClientResponse
{

public function __construct(
private readonly HttpClientRequest $request,
private readonly string $body,
private readonly ?OpenSSLCertificate $tlsCertificate,
) {
}


public function getBody(): string
{
return $this->body;
}


/**
* @throws HttpClientTlsCertificateNotAvailableException
* @throws HttpClientTlsCertificateNotCapturedException
*/
public function getTlsCertificate(): OpenSSLCertificate
{
$scheme = parse_url($this->request->getUrl(), PHP_URL_SCHEME);
if (!is_string($scheme) || strtolower($scheme) !== 'https') {
throw new HttpClientTlsCertificateNotAvailableException($this->request->getUrl());
}
if (!$this->request->getTlsCaptureCertificate() || !$this->tlsCertificate) {
throw new HttpClientTlsCertificateNotCapturedException();
}
return $this->tlsCertificate;
}

}
Loading

0 comments on commit 8255311

Please sign in to comment.