Skip to content

Commit

Permalink
Add websocket feature
Browse files Browse the repository at this point in the history
  • Loading branch information
parsilver committed Dec 23, 2023
1 parent 0f3a9a0 commit 515471e
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 4 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"require": {
"php": "^8.1",
"farzai/support": "^1.2",
"farzai/transport": "^1.2"
"farzai/transport": "^1.2",
"phrity/websocket": "^2.0"
},
"require-dev": {
"pestphp/pest": "^2.15",
Expand Down
33 changes: 33 additions & 0 deletions example/stream-trade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

require_once __DIR__.'/../vendor/autoload.php';

use Farzai\Bitkub\ClientBuilder;
use Farzai\Bitkub\WebSocket\Message;
use Farzai\Bitkub\WebSocketClient;

$client = ClientBuilder::create()
->setCredentials('YOUR_API_KEY', 'YOUR_SECRET')
->build();

$websocket = new WebSocketClient($client);

$websocket->listen([
'market.trade.thb_btc' => [
function (Message $message) {
echo $message->json('sym').PHP_EOL;
},
],
]);

$websocket->listen([
'market.trade.thb_eth' => function (Message $message) {
echo $message->json('sym').PHP_EOL;
},
]);

$websocket->listen('market.trade.thb_ada', function (Message $message) {
echo $message->json('sym').PHP_EOL;
});

$websocket->run();
10 changes: 10 additions & 0 deletions src/Contracts/WebSocketEngineInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Farzai\Bitkub\Contracts;

interface WebSocketEngineInterface
{
public function addListener(string $event, callable $listener);

public function run(): void;
}
6 changes: 3 additions & 3 deletions src/Endpoints/AbstractEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

namespace Farzai\Bitkub\Endpoints;

use Farzai\Bitkub\Client;
use Farzai\Bitkub\Contracts\ClientInterface;
use Farzai\Bitkub\Requests\PendingRequest;

abstract class AbstractEndpoint
{
protected Client $client;
protected ClientInterface $client;

public function __construct(Client $client)
public function __construct(ClientInterface $client)
{
$this->client = $client;
}
Expand Down
14 changes: 14 additions & 0 deletions src/UriFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Farzai\Bitkub;

use Phrity\Net\Uri;
use Psr\Http\Message\UriInterface;

class UriFactory
{
public static function createFromUri(string $uri): UriInterface

Check warning on line 10 in src/UriFactory.php

View check run for this annotation

Codecov / codecov/patch

src/UriFactory.php#L10

Added line #L10 was not covered by tests
{
return new Uri($uri);

Check warning on line 12 in src/UriFactory.php

View check run for this annotation

Codecov / codecov/patch

src/UriFactory.php#L12

Added line #L12 was not covered by tests
}
}
73 changes: 73 additions & 0 deletions src/WebSocket/Engine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Farzai\Bitkub\WebSocket;

use Farzai\Bitkub\Contracts\WebSocketEngineInterface;
use Farzai\Support\Carbon;
use Psr\Log\LoggerInterface;
use WebSocket\Client as WebSocketClient;
use WebSocket\Connection as WebSocketConnection;
use WebSocket\Message\Message as WebSocketMessage;
use WebSocket\Middleware as WebSocketMiddleware;

class Engine implements WebSocketEngineInterface
{
/**
* @var array<string, callable[]>
*/
private array $listeners = [];

public function __construct(

Check warning on line 20 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L20

Added line #L20 was not covered by tests
private LoggerInterface $logger,
) {
}

Check warning on line 23 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L23

Added line #L23 was not covered by tests

public function addListener(string $event, callable $listener)

Check warning on line 25 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L25

Added line #L25 was not covered by tests
{
$this->listeners[$event][] = $listener;

Check warning on line 27 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L27

Added line #L27 was not covered by tests

return $this;

Check warning on line 29 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L29

Added line #L29 was not covered by tests
}

public function run(): void

Check warning on line 32 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L32

Added line #L32 was not covered by tests
{
$events = array_unique(array_keys($this->listeners));

Check warning on line 34 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L34

Added line #L34 was not covered by tests

$client = new WebSocketClient('wss://api.bitkub.com/websocket-api/'.implode(',', $events));

Check warning on line 36 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L36

Added line #L36 was not covered by tests

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

Check warning on line 40 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L38-L40

Added lines #L38 - L40 were not covered by tests

$client->onText(function (WebSocketClient $client, WebSocketConnection $connection, WebSocketMessage $message) {
$receivedAt = Carbon::now();

Check warning on line 43 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L42-L43

Added lines #L42 - L43 were not covered by tests

$data = @json_decode($message->getContent(), true) ?? [];
if (! isset($data['stream'])) {
$this->logger->warning('[WebSocket] - '.Carbon::now()->format('Y-m-d H:i:s').' - Unknown data: '.$message->getContent());

Check warning on line 47 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L45-L47

Added lines #L45 - L47 were not covered by tests

return;

Check warning on line 49 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L49

Added line #L49 was not covered by tests
}

$event = $data['stream'];
if (! isset($this->listeners[$event])) {
$this->logger->warning('[WebSocket] - '.Carbon::now()->format('Y-m-d H:i:s').' - Unknown event: '.$event);

Check warning on line 54 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L52-L54

Added lines #L52 - L54 were not covered by tests

return;

Check warning on line 56 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L56

Added line #L56 was not covered by tests
}

$message = new Message(
$message->getContent(),
$receivedAt->toDateTimeImmutable(),
);

Check warning on line 62 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L59-L62

Added lines #L59 - L62 were not covered by tests

foreach ($this->listeners[$event] as $listener) {
$this->logger->info('[WebSocket] - '.Carbon::now()->format('Y-m-d H:i:s').' - Event: '.$event);

Check warning on line 65 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L64-L65

Added lines #L64 - L65 were not covered by tests

$listener($message);

Check warning on line 67 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L67

Added line #L67 was not covered by tests
}
});

Check warning on line 69 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L69

Added line #L69 was not covered by tests

$client->start();

Check warning on line 71 in src/WebSocket/Engine.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Engine.php#L71

Added line #L71 was not covered by tests
}
}
97 changes: 97 additions & 0 deletions src/WebSocket/Message.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace Farzai\Bitkub\WebSocket;

use ArrayAccess;
use DateTimeImmutable;
use Farzai\Support\Arr;

class Message implements \JsonSerializable, ArrayAccess
{
private string $body;

private $jsonDecoded;

private DateTimeImmutable $receivedAt;

public function __construct(string $body, DateTimeImmutable $receivedAt)

Check warning on line 17 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L17

Added line #L17 was not covered by tests
{
$this->body = $body;
$this->jsonDecoded = @json_decode($body, true) ?? false;
$this->receivedAt = $receivedAt;

Check warning on line 21 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L19-L21

Added lines #L19 - L21 were not covered by tests
}

public function getBody(): string

Check warning on line 24 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L24

Added line #L24 was not covered by tests
{
return $this->body;

Check warning on line 26 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L26

Added line #L26 was not covered by tests
}

public function getReceivedAt(): DateTimeImmutable

Check warning on line 29 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L29

Added line #L29 was not covered by tests
{
return $this->receivedAt;

Check warning on line 31 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L31

Added line #L31 was not covered by tests
}

public function json($key = null)

Check warning on line 34 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L34

Added line #L34 was not covered by tests
{
if ($key === null) {
return $this->jsonDecoded ?: null;

Check warning on line 37 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L36-L37

Added lines #L36 - L37 were not covered by tests
}

return Arr::get($this->jsonDecoded, $key);

Check warning on line 40 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L40

Added line #L40 was not covered by tests
}

public function __toString(): string

Check warning on line 43 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L43

Added line #L43 was not covered by tests
{
return $this->getBody();

Check warning on line 45 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L45

Added line #L45 was not covered by tests
}

public function jsonSerialize(): array

Check warning on line 48 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L48

Added line #L48 was not covered by tests
{
return $this->toArray();

Check warning on line 50 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L50

Added line #L50 was not covered by tests
}

public function toArray(): array

Check warning on line 53 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L53

Added line #L53 was not covered by tests
{
return $this->jsonDecoded;

Check warning on line 55 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L55

Added line #L55 was not covered by tests
}

public function offsetExists($offset): bool

Check warning on line 58 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L58

Added line #L58 was not covered by tests
{
return isset($this->jsonDecoded[$offset]);

Check warning on line 60 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L60

Added line #L60 was not covered by tests
}

public function offsetGet($offset): mixed

Check warning on line 63 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L63

Added line #L63 was not covered by tests
{
return $this->jsonDecoded[$offset];

Check warning on line 65 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L65

Added line #L65 was not covered by tests
}

public function offsetSet($offset, $value): void

Check warning on line 68 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L68

Added line #L68 was not covered by tests
{
$this->jsonDecoded[$offset] = $value;

Check warning on line 70 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L70

Added line #L70 was not covered by tests
}

public function offsetUnset($offset): void

Check warning on line 73 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L73

Added line #L73 was not covered by tests
{
unset($this->jsonDecoded[$offset]);

Check warning on line 75 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L75

Added line #L75 was not covered by tests
}

public function __get($name)

Check warning on line 78 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L78

Added line #L78 was not covered by tests
{
return $this->jsonDecoded[$name] ?? null;

Check warning on line 80 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L80

Added line #L80 was not covered by tests
}

public function __isset($name): bool

Check warning on line 83 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L83

Added line #L83 was not covered by tests
{
return isset($this->jsonDecoded[$name]);

Check warning on line 85 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L85

Added line #L85 was not covered by tests
}

public function __set($name, $value): void

Check warning on line 88 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L88

Added line #L88 was not covered by tests
{
$this->jsonDecoded[$name] = $value;

Check warning on line 90 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L90

Added line #L90 was not covered by tests
}

public function __unset($name): void

Check warning on line 93 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L93

Added line #L93 was not covered by tests
{
unset($this->jsonDecoded[$name]);

Check warning on line 95 in src/WebSocket/Message.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocket/Message.php#L95

Added line #L95 was not covered by tests
}
}
93 changes: 93 additions & 0 deletions src/WebSocketClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace Farzai\Bitkub;

use Farzai\Bitkub\Contracts\ClientInterface;
use Farzai\Bitkub\Responses\Message;
use Farzai\Transport\Transport;
use Psr\Http\Message\RequestInterface as PsrRequestInterface;
use Psr\Log\LoggerInterface;

final class WebSocketClient implements ClientInterface
{
private ClientInterface $client;

private Contracts\WebSocketEngineInterface $websocket;

/**
* @var array<string, array<mixed>>
*/
private array $listeners = [];

public function __construct(ClientInterface $client)

Check warning on line 22 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L22

Added line #L22 was not covered by tests
{
$this->client = $client;
$this->websocket = new WebSocket\Engine($this->getLogger());

Check warning on line 25 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L24-L25

Added lines #L24 - L25 were not covered by tests
}

public function getConfig(): array

Check warning on line 28 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L28

Added line #L28 was not covered by tests
{
return $this->client->getConfig();

Check warning on line 30 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L30

Added line #L30 was not covered by tests
}

public function getTransport(): Transport

Check warning on line 33 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L33

Added line #L33 was not covered by tests
{
return $this->client->getTransport();

Check warning on line 35 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L35

Added line #L35 was not covered by tests
}

public function getLogger(): LoggerInterface

Check warning on line 38 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L38

Added line #L38 was not covered by tests
{
return $this->client->getLogger();

Check warning on line 40 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L40

Added line #L40 was not covered by tests
}

public function sendRequest(PsrRequestInterface $request)

Check warning on line 43 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L43

Added line #L43 was not covered by tests
{
return $this->client->sendRequest($request);

Check warning on line 45 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L45

Added line #L45 was not covered by tests
}

/**
* Add event listener.
*
* @example $websocket->listen('market.trade.thb_btc', function (Message $message) {
* echo $message->json('sym').PHP_EOL;
* });
*
* @param array<string, callable|array<callable>>|string $listeners
*/
public function listen($listeners)

Check warning on line 57 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L57

Added line #L57 was not covered by tests
{
if (func_num_args() === 2) {
$eventName = func_get_arg(0);
$listener = is_array(func_get_arg(1)) ? func_get_arg(1) : [func_get_arg(1)];

Check warning on line 61 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L59-L61

Added lines #L59 - L61 were not covered by tests

$listeners = [$eventName => $listener];

Check warning on line 63 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L63

Added line #L63 was not covered by tests
}

foreach ($listeners as $event => $listener) {
if (! isset($this->listeners[$event])) {
$this->listeners[$event] = [];

Check warning on line 68 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L66-L68

Added lines #L66 - L68 were not covered by tests
}

if (is_callable($listener)) {
$this->listeners[$event][] = $listener;
} elseif (is_array($listener)) {
foreach ($listener as $callback) {
$this->listeners[$event][] = $callback;

Check warning on line 75 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L71-L75

Added lines #L71 - L75 were not covered by tests
}
}
}

return $this;

Check warning on line 80 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L80

Added line #L80 was not covered by tests
}

public function run()

Check warning on line 83 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L83

Added line #L83 was not covered by tests
{
foreach ($this->listeners as $event => $listeners) {
foreach ($listeners as $listener) {
$this->websocket->addListener($event, $listener);

Check warning on line 87 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L85-L87

Added lines #L85 - L87 were not covered by tests
}
}

$this->websocket->run();

Check warning on line 91 in src/WebSocketClient.php

View check run for this annotation

Codecov / codecov/patch

src/WebSocketClient.php#L91

Added line #L91 was not covered by tests
}
}

0 comments on commit 515471e

Please sign in to comment.