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
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, windows-latest]
php: [8.2, 8.1]
php: [8.4, 8.3, 8.2]
stability: [prefer-lowest, prefer-stable]

name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/.fleet
.php_cs
.php_cs.cache
.phpunit.cache
.phpunit.result.cache
build
composer.lock
Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
}
],
"require": {
"php": "^8.1",
"php": "^8.2",
"farzai/support": "^1.2",
"farzai/transport": "^1.2",
"farzai/transport": "^2.1",
"phrity/websocket": "^3.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.8",
"pestphp/pest": "^2.15",
"laravel/pint": "^1.0",
"spatie/ray": "^1.28"
Expand Down
19 changes: 8 additions & 11 deletions src/ClientBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ final class ClientBuilder
*/
public static function create()
{
return new self();
return new self;
}

/**
Expand Down Expand Up @@ -98,17 +98,17 @@ public function build()
{
$this->ensureConfigIsValid();

$logger = $this->logger ?? new NullLogger();
$logger = $this->logger ?? new NullLogger;

$builder = TransportBuilder::make()
->withBaseUri(sprintf('https://%s', self::DEFAULT_HOST))
->setLogger($logger);

$builder = TransportBuilder::make();
if ($this->httpClient) {
$builder->setClient($this->httpClient);
$builder = $builder->setClient($this->httpClient);
}

$builder->setLogger($logger);

$transport = $builder->build();
$transport->setUri(sprintf('https://%s', self::DEFAULT_HOST));

$client = new Client(
config: $this->config,
Expand All @@ -130,8 +130,5 @@ private function ensureConfigIsValid(): void
}
}

final public function __construct()
{

}
final public function __construct() {}
}
4 changes: 2 additions & 2 deletions src/Endpoints/MarketEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function symbols(): ResponseInterface
/**
* Get ticker information.
*
* @param string $symbol (optional) The symbol (e.g. btc_thb)
* @param string $symbol (optional) The symbol (e.g. btc_thb)
*
* @response
* {
Expand Down Expand Up @@ -388,7 +388,7 @@ public function balances(): ResponseInterface
* List all open orders of the given symbol.
* Note : The client_id of this API response is the input body field name client_id , was inputted by the user of APIs.
*
* @param string $sym The symbol (e.g. btc_thb)
* @param string $sym The symbol (e.g. btc_thb)
*
* @response
* {
Expand Down
20 changes: 11 additions & 9 deletions src/Requests/PendingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use Farzai\Bitkub\Contracts\RequestInterceptor;
use Farzai\Bitkub\Responses\ResponseWithValidateErrorCode;
use Farzai\Transport\Contracts\ResponseInterface;
use Farzai\Transport\Request;
use Farzai\Transport\RequestBuilder;
use Farzai\Transport\Response;
use Psr\Http\Message\RequestInterface as PsrRequestInterface;
use Psr\Http\Message\ResponseInterface as PsrResponseInterface;
Expand Down Expand Up @@ -146,25 +146,27 @@ public function createRequest(string $method, string $path, array $options = [])
// Normalize path
$path = '/'.trim($path, '/');

$builder = (new RequestBuilder)->method($method)->uri($path);

// Query
if (isset($options['query']) && is_array($options['query']) && ! empty($options['query'])) {
$path .= '?'.http_build_query($options['query']);
$builder = $builder->withQuery($options['query']);
}

// Set body
if (isset($options['body'])) {
$body = $options['body'];

// Convert array to json
if (is_array($body)) {
$body = json_encode($body);
}
$builder = is_array($body)
? $builder->withJson($body)
: $builder->withBody($body);
}

// Set headers
$headers = $options['headers'] ?? [];
if (! empty($options['headers'])) {
$builder = $builder->withHeaders($options['headers']);
}

return new Request($method, $path, $headers, $body ?? null);
return $builder->build();
}

/**
Expand Down
116 changes: 105 additions & 11 deletions src/Responses/AbstractResponseDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
namespace Farzai\Bitkub\Responses;

use Farzai\Transport\Contracts\ResponseInterface;
use Farzai\Transport\Traits\PsrResponseTrait;
use Psr\Http\Message\RequestInterface as PsrRequestInterface;
use Psr\Http\Message\StreamInterface;

abstract class AbstractResponseDecorator implements ResponseInterface
{
use PsrResponseTrait;

protected ResponseInterface $response;

/**
Expand Down Expand Up @@ -45,11 +43,11 @@ public function headers(): array
}

/**
* Check if the response is successfull.
* Check if the response is successful.
*/
public function isSuccessfull(): bool
public function isSuccessful(): bool
{
return $this->response->isSuccessfull();
return $this->response->isSuccessful();
}

/**
Expand All @@ -61,15 +59,31 @@ public function json(?string $key = null): mixed
}

/**
* Throw an exception if the response is not successfull.
*
* @param callable<\Farzai\Transport\Contracts\ResponseInterface> $callback Custom callback to throw an exception.
* Return the json decoded response or null.
*/
public function jsonOrNull(?string $key = null): mixed
{
return $this->response->jsonOrNull($key);
}

/**
* Return the response as an array.
*/
public function toArray(): array
{
return $this->response->toArray();
}

/**
* Throw an exception if the response is not successful.
*
* @throws \Psr\Http\Client\ClientExceptionInterface
*/
public function throw(?callable $callback = null)
public function throw(?callable $callback = null): static
{
return $this->response->throw($callback);
$this->response->throw($callback);

return $this;
}

/**
Expand All @@ -79,4 +93,84 @@ public function getPsrRequest(): PsrRequestInterface
{
return $this->response->getPsrRequest();
}

// PSR-7 ResponseInterface delegation

public function getStatusCode(): int
{
return $this->response->getStatusCode();
}

public function withStatus(int $code, string $reasonPhrase = ''): static
{
return $this->cloneWithResponse($this->response->withStatus($code, $reasonPhrase));
}

public function getReasonPhrase(): string
{
return $this->response->getReasonPhrase();
}

public function getProtocolVersion(): string
{
return $this->response->getProtocolVersion();
}

public function withProtocolVersion(string $version): static
{
return $this->cloneWithResponse($this->response->withProtocolVersion($version));
}

public function getHeaders(): array
{
return $this->response->getHeaders();
}

public function hasHeader(string $name): bool
{
return $this->response->hasHeader($name);
}

public function getHeader(string $name): array
{
return $this->response->getHeader($name);
}

public function getHeaderLine(string $name): string
{
return $this->response->getHeaderLine($name);
}

public function withHeader(string $name, $value): static
{
return $this->cloneWithResponse($this->response->withHeader($name, $value));
}

public function withAddedHeader(string $name, $value): static
{
return $this->cloneWithResponse($this->response->withAddedHeader($name, $value));
}

public function withoutHeader(string $name): static
{
return $this->cloneWithResponse($this->response->withoutHeader($name));
}

public function getBody(): StreamInterface
{
return $this->response->getBody();
}

public function withBody(StreamInterface $body): static
{
return $this->cloneWithResponse($this->response->withBody($body));
}

private function cloneWithResponse(ResponseInterface $response): static
{
$clone = clone $this;
$clone->response = $response;

return $clone;
}
}
11 changes: 7 additions & 4 deletions src/Responses/ResponseWithValidateErrorCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@
class ResponseWithValidateErrorCode extends AbstractResponseDecorator
{
/**
* Throw an exception if the response is not successfull.
* Throw an exception if the response is not successful.
*
*
* @throws \Psr\Http\Client\ClientExceptionInterface
*/
public function throw(?callable $callback = null)
public function throw(?callable $callback = null): static
{
return parent::throw($callback ?? function (ResponseInterface $response, ?\Exception $e) use ($callback) {
if ($this->json('error') !== null && $this->json('error') !== 0) {
parent::throw($callback ?? function (ResponseInterface $response, ?\Exception $e) use ($callback) {
$errorCode = $this->json('error');
if ($errorCode !== null && $errorCode !== 0) {
throw new BitkubResponseErrorCodeException($response);
}

return $callback ? $callback($response, $e) : $response;
});

return $this;
}
}
2 changes: 1 addition & 1 deletion src/WebSocket/Endpoints/LiveOrderBookEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class LiveOrderBookEndpoint extends AbstractEndpoint
* echo $message->json('sym');
* });
*
* @param string|int $symbol Symbol name or id.
* @param string|int $symbol Symbol name or id.
* @param callable|array<callable> $listeners
*/
public function listen($symbol, $listeners)
Expand Down
7 changes: 3 additions & 4 deletions src/WebSocket/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ class Engine implements WebSocketEngineInterface
{
public function __construct(
private LoggerInterface $logger,
) {
}
) {}

public function handle(array $listeners): void
{
Expand All @@ -28,8 +27,8 @@ public function handle(array $listeners): void
$client = new WebSocketClient('wss://api.bitkub.com/websocket-api/'.implode(',', $events));

$client
->addMiddleware(new WebSocketMiddleware\CloseHandler())
->addMiddleware(new WebSocketMiddleware\PingResponder());
->addMiddleware(new WebSocketMiddleware\CloseHandler)
->addMiddleware(new WebSocketMiddleware\PingResponder);

$client->onText(function (WebSocketClient $client, WebSocketConnection $connection, WebSocketMessage $message) use ($listeners) {
$receivedAt = Carbon::now();
Expand Down
36 changes: 36 additions & 0 deletions tests/ClientBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,39 @@

$client->market()->balances();
})->throws(InvalidArgumentException::class, 'Secret key is required');

it('can set custom http client', function () {
$httpClient = $this->createMock(\Psr\Http\Client\ClientInterface::class);

$client = ClientBuilder::create()
->setCredentials('test', 'secret')
->setHttpClient($httpClient)
->build();

expect($client)->toBeInstanceOf(Client::class);
});

it('can set custom logger', function () {
$logger = $this->createMock(\Psr\Log\LoggerInterface::class);

$client = ClientBuilder::create()
->setCredentials('test', 'secret')
->setLogger($logger)
->build();

expect($client)->toBeInstanceOf(Client::class);
});

it('can set retries', function () {
$client = ClientBuilder::create()
->setCredentials('test', 'secret')
->setRetries(5)
->build();

expect($client)->toBeInstanceOf(Client::class);
});

it('should throw exception when retries is negative', function () {
ClientBuilder::create()
->setRetries(-1);
})->throws(\InvalidArgumentException::class, 'Retries must be greater than or equal to 0.');
Loading
Loading