diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d5071af --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +/.github export-ignore +/.vscode export-ignore +/tests export-ignore +.gitattributes export-ignore +/bin export-ignore \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..84136de --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,28 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get the version + id: get_version + if: startsWith(github.ref, 'refs/tags/') + run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} + + - name: Release + uses: softprops/action-gh-release@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag_name: ${{ github.ref }} + name: Release ${{ steps.get_version.outputs.VERSION }} + draft: false + prerelease: false + generate_release_notes: true \ No newline at end of file diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2a984dc..05aa6e2 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -10,7 +10,8 @@ jobs: matrix: os: [ubuntu-latest] php-version: ['7.1', '7.2', '7.3', '7.4', '8.0'] - max-parallel: 3 + max-parallel: 20 + fail-fast: false steps: - name: Checkout uses: actions/checkout@v2 @@ -25,10 +26,11 @@ jobs: - name: Run Analyse run: | composer analyse src + - name: Setup Consul + run: docker run -d --name=dev-consul -e CONSUL_BIND_INTERFACE=eth0 --net=host consul:1.15.4 - name: Setup Services run: | - docker run -d --name jsonrpc -p 9501:9501 -p 9502:9502 -p 9503:9503 -p 9504:9504 limingxinleo/hyperf-jsonrpc-demo:latest - docker run -d --name dev-consul -e CONSUL_BIND_INTERFACE=eth0 --network host consul + docker run -d --name jsonrpc -p 9501:9501 -p 9502:9502 -p 9503:9503 -p 9504:9504 -p 9505:9505 iisiam/hyperf-rpc-demo:latest sleep 10 php ./tests/register.php - name: Run Test Cases diff --git a/.gitignore b/.gitignore index 5aaa618..d52b9bd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ composer.lock *.bak /phpunit.xml /.phpunit.result.cache +.idea +.bashrc \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index c087f33..fb874a4 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -1,12 +1,11 @@ setTransporter(new GuzzleHttpTransporter('127.0.0.1', 9502)); -$metadata->setRegistry(new ConsulRegistry(['uri' => 'http://127.0.0.1:8500'])); +$metadata = (new Metadata('CalculatorService')) + ->withTransporter(new GuzzleHttpTransporter('127.0.0.1', 9502)) + ->withRegistry(new ConsulRegistry(['uri' => 'http://127.0.0.1:8500'])); ServiceManager::register('CalculatorService', $metadata); -~~~ +``` ### Register default registry -~~~php +```php use FriendsOfHyperf\Jet\RegistryManager; use FriendsOfHyperf\Jet\Registry\ConsulRegistry; RegistryManager::register(RegistryManager::DEFAULT, new ConsulRegistry(['uri' => $uri, 'timeout' => 1])); -~~~ +``` > In Laravel project, Add to `boot()` in `App/Providers/AppServiceProvider.php` @@ -47,16 +47,16 @@ RegistryManager::register(RegistryManager::DEFAULT, new ConsulRegistry(['uri' => ### Call by ClientFactory -~~~php +```php use FriendsOfHyperf\Jet\ClientFactory; $client = ClientFactory::create('CalculatorService'); var_dump($client->add(1, 20)); -~~~ +``` ### Call by custom client -~~~php +```php use FriendsOfHyperf\Jet\Client; use FriendsOfHyperf\Jet\Transporter\GuzzleHttpTransporter; use FriendsOfHyperf\Jet\Registry\ConsulRegistry; @@ -68,13 +68,9 @@ class CalculatorService extends Client { public function __construct($service = 'CalculatorService') { - $metadata = new Metadata($service); - - // Custom transporter - $metadata->setTransporter(new GuzzleHttpTransporter('127.0.0.1', 9502)); - - // Custom registry - $metadata->setRegistry(new ConsulRegistry(['uri' => 'http://127.0.0.1:8500'])); + $metadata = (new Metadata($service)) + ->withTransporter(new GuzzleHttpTransporter('127.0.0.1', 9502)) + ->withRegistry(new ConsulRegistry(['uri' => 'http://127.0.0.1:8500'])); parent::__construct($metadata); } @@ -82,11 +78,11 @@ class CalculatorService extends Client $service = new CalculatorService; var_dump($service->add(3, 10)); -~~~ +``` ### Call by custom facade -~~~php +```php use FriendsOfHyperf\Jet\Facade; use FriendsOfHyperf\Jet\ClientFactory; @@ -103,22 +99,52 @@ class Calculator extends Facade } var_dump(Calculator::add(rand(0, 100), rand(0, 100))); -~~~ +``` ## Coroutine support in Hyperf -~~~php -// config/autoload/annotations.php +- Aspect + +```php +clientFactory = $clientFactory; + } + + public function process(ProceedingJoinPoint $proceedingJoinPoint) + { + $instance = $proceedingJoinPoint->getInstance(); + $config = (function () { return $this->config; })->call($instance); + + return $this->clientFactory->create($config); + } +} +``` + +- Config `config/autoload/aspects.php` + +```php [ - // ... - 'class_map' => [ - GuzzleHttp\Client::class => BASE_PATH . '/vendor/friendsofhyperf/jet/classmap/GuzzleHttp/Client.php', - ], - ], + 'App\Aspect\GuzzleHttpTransporterAspect', ]; -~~~ +``` diff --git a/bootstrap.php b/bootstrap.php index dd9ce5e..7586ea0 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -2,12 +2,11 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ ! defined('BASE_PATH') && define('BASE_PATH', __DIR__); diff --git a/classmap/GuzzleHttp/Client.php b/classmap/GuzzleHttp/Client.php deleted file mode 100644 index 79be25f..0000000 --- a/classmap/GuzzleHttp/Client.php +++ /dev/null @@ -1,489 +0,0 @@ - 'http://www.foo.com/1.0/', - * 'timeout' => 0, - * 'allow_redirects' => false, - * 'proxy' => '192.168.16.1:10' - * ]); - * - * Client configuration settings include the following options: - * - * - handler: (callable) Function that transfers HTTP requests over the - * wire. The function is called with a Psr7\Http\Message\RequestInterface - * and array of transfer options, and must return a - * GuzzleHttp\Promise\PromiseInterface that is fulfilled with a - * Psr7\Http\Message\ResponseInterface on success. - * If no handler is provided, a default handler will be created - * that enables all of the request options below by attaching all of the - * default middleware to the handler. - * - base_uri: (string|UriInterface) Base URI of the client that is merged - * into relative URIs. Can be a string or instance of UriInterface. - * - **: any request option - * - * @param array $config client configuration settings - * - * @see \GuzzleHttp\RequestOptions for a list of available request options. - */ - public function __construct(array $config = []) - { - $inCoroutine = Coroutine::inCoroutine(); - if (! isset($config['handler'])) { - // 对应的 Handler 可以按需选择 CoroutineHandler 或 PoolHandler - $config['handler'] = HandlerStack::create($inCoroutine ? new CoroutineHandler() : null); - } elseif ($inCoroutine && $config['handler'] instanceof HandlerStack) { - $config['handler']->setHandler(new CoroutineHandler()); - } elseif (! is_callable($config['handler'])) { - throw new \InvalidArgumentException('handler must be a callable'); - } - - // Convert the base_uri to a UriInterface - if (isset($config['base_uri'])) { - $config['base_uri'] = Psr7\uri_for($config['base_uri']); - } - - $this->configureDefaults($config); - } - - /** - * @param string $method - * @param array $args - * - * @return PromiseInterface|ResponseInterface - * - * @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0. - */ - public function __call($method, $args) - { - if (\count($args) < 1) { - throw new InvalidArgumentException('Magic request methods require a URI and optional options array'); - } - - $uri = $args[0]; - $opts = $args[1] ?? []; - - return \substr($method, -5) === 'Async' - ? $this->requestAsync(\substr($method, 0, -5), $uri, $opts) - : $this->request($method, $uri, $opts); - } - - /** - * Asynchronously send an HTTP request. - * - * @param array $options Request options to apply to the given - * request and to the transfer. See \GuzzleHttp\RequestOptions. - */ - public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface - { - // Merge the base URI into the request URI if needed. - $options = $this->prepareDefaults($options); - - return $this->transfer( - $request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')), - $options - ); - } - - /** - * Send an HTTP request. - * - * @param array $options Request options to apply to the given - * request and to the transfer. See \GuzzleHttp\RequestOptions. - * - * @throws GuzzleException - */ - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - $options[RequestOptions::SYNCHRONOUS] = true; - return $this->sendAsync($request, $options)->wait(); - } - - /** - * The HttpClient PSR (PSR-18) specify this method. - * - * {@inheritDoc} - */ - public function sendRequest(RequestInterface $request): ResponseInterface - { - $options[RequestOptions::SYNCHRONOUS] = true; - $options[RequestOptions::ALLOW_REDIRECTS] = false; - $options[RequestOptions::HTTP_ERRORS] = false; - - return $this->sendAsync($request, $options)->wait(); - } - - /** - * Create and send an asynchronous HTTP request. - * - * Use an absolute path to override the base path of the client, or a - * relative path to append to the base path of the client. The URL can - * contain the query string as well. Use an array to provide a URL - * template and additional variables to use in the URL template expansion. - * - * @param string $method HTTP method - * @param string|UriInterface $uri URI object or string - * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. - */ - public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface - { - $options = $this->prepareDefaults($options); - // Remove request modifying parameter because it can be done up-front. - $headers = $options['headers'] ?? []; - $body = $options['body'] ?? null; - $version = $options['version'] ?? '1.1'; - // Merge the URI into the base URI. - $uri = $this->buildUri(Psr7\Utils::uriFor($uri), $options); - if (\is_array($body)) { - throw $this->invalidBody(); - } - $request = new Psr7\Request($method, $uri, $headers, $body, $version); - // Remove the option so that they are not doubly-applied. - unset($options['headers'], $options['body'], $options['version']); - - return $this->transfer($request, $options); - } - - /** - * Create and send an HTTP request. - * - * Use an absolute path to override the base path of the client, or a - * relative path to append to the base path of the client. The URL can - * contain the query string as well. - * - * @param string $method HTTP method - * @param string|UriInterface $uri URI object or string - * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. - * - * @throws GuzzleException - */ - public function request(string $method, $uri = '', array $options = []): ResponseInterface - { - $options[RequestOptions::SYNCHRONOUS] = true; - return $this->requestAsync($method, $uri, $options)->wait(); - } - - /** - * Get a client configuration option. - * - * These options include default request options of the client, a "handler" - * (if utilized by the concrete client), and a "base_uri" if utilized by - * the concrete client. - * - * @param null|string $option the config option to retrieve - * - * @return mixed - * - * @deprecated Client::getConfig will be removed in guzzlehttp/guzzle:8.0. - */ - public function getConfig(?string $option = null) - { - return $option === null - ? $this->config - : (isset($this->config[$option]) ? $this->config[$option] : null); - } - - private function buildUri(UriInterface $uri, array $config): UriInterface - { - if (isset($config['base_uri'])) { - $uri = Psr7\UriResolver::resolve(Psr7\Utils::uriFor($config['base_uri']), $uri); - } - - if (isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) { - $idnOptions = ($config['idn_conversion'] === true) ? \IDNA_DEFAULT : $config['idn_conversion']; - $uri = Utils::idnUriConvert($uri, $idnOptions); - } - - return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri; - } - - /** - * Configures the default options for a client. - */ - private function configureDefaults(array $config): void - { - $defaults = [ - 'allow_redirects' => RedirectMiddleware::$defaultSettings, - 'http_errors' => true, - 'decode_content' => true, - 'verify' => true, - 'cookies' => false, - 'idn_conversion' => false, - ]; - - // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set. - - // We can only trust the HTTP_PROXY environment variable in a CLI - // process due to the fact that PHP has no reliable mechanism to - // get environment variables that start with "HTTP_". - if (\PHP_SAPI === 'cli' && ($proxy = Utils::getenv('HTTP_PROXY'))) { - $defaults['proxy']['http'] = $proxy; - } - - if ($proxy = Utils::getenv('HTTPS_PROXY')) { - $defaults['proxy']['https'] = $proxy; - } - - if ($noProxy = Utils::getenv('NO_PROXY')) { - $cleanedNoProxy = \str_replace(' ', '', $noProxy); - $defaults['proxy']['no'] = \explode(',', $cleanedNoProxy); - } - - $this->config = $config + $defaults; - - if (! empty($config['cookies']) && $config['cookies'] === true) { - $this->config['cookies'] = new CookieJar(); - } - - // Add the default user-agent header. - if (! isset($this->config['headers'])) { - $this->config['headers'] = ['User-Agent' => Utils::defaultUserAgent()]; - } else { - // Add the User-Agent header if one was not already set. - foreach (\array_keys($this->config['headers']) as $name) { - if (\strtolower($name) === 'user-agent') { - return; - } - } - $this->config['headers']['User-Agent'] = Utils::defaultUserAgent(); - } - } - - /** - * Merges default options into the array. - * - * @param array $options Options to modify by reference - */ - private function prepareDefaults(array $options): array - { - $defaults = $this->config; - - if (! empty($defaults['headers'])) { - // Default headers are only added if they are not present. - $defaults['_conditional'] = $defaults['headers']; - unset($defaults['headers']); - } - - // Special handling for headers is required as they are added as - // conditional headers and as headers passed to a request ctor. - if (\array_key_exists('headers', $options)) { - // Allows default headers to be unset. - if ($options['headers'] === null) { - $defaults['_conditional'] = []; - unset($options['headers']); - } elseif (! \is_array($options['headers'])) { - throw new InvalidArgumentException('headers must be an array'); - } - } - - // Shallow merge defaults underneath options. - $result = $options + $defaults; - - // Remove null values. - foreach ($result as $k => $v) { - if ($v === null) { - unset($result[$k]); - } - } - - return $result; - } - - /** - * Transfers the given request and applies request options. - * - * The URI of the request is not modified and the request options are used - * as-is without merging in default options. - * - * @param array $options see \GuzzleHttp\RequestOptions - */ - private function transfer(RequestInterface $request, array $options): PromiseInterface - { - $request = $this->applyOptions($request, $options); - /** @var HandlerStack $handler */ - $handler = $options['handler']; - - try { - return P\Create::promiseFor($handler($request, $options)); - } catch (\Exception $e) { - return P\Create::rejectionFor($e); - } - } - - /** - * Applies the array of request options to a request. - */ - private function applyOptions(RequestInterface $request, array &$options): RequestInterface - { - $modify = [ - 'set_headers' => [], - ]; - - if (isset($options['headers'])) { - $modify['set_headers'] = $options['headers']; - unset($options['headers']); - } - - if (isset($options['form_params'])) { - if (isset($options['multipart'])) { - throw new InvalidArgumentException('You cannot use ' - . 'form_params and multipart at the same time. Use the ' - . 'form_params option if you want to send application/' - . 'x-www-form-urlencoded requests, and the multipart ' - . 'option to send multipart/form-data requests.'); - } - $options['body'] = \http_build_query($options['form_params'], '', '&'); - unset($options['form_params']); - // Ensure that we don't have the header in different case and set the new value. - $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); - $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; - } - - if (isset($options['multipart'])) { - $options['body'] = new Psr7\MultipartStream($options['multipart']); - unset($options['multipart']); - } - - if (isset($options['json'])) { - $options['body'] = Utils::jsonEncode($options['json']); - unset($options['json']); - // Ensure that we don't have the header in different case and set the new value. - $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); - $options['_conditional']['Content-Type'] = 'application/json'; - } - - if (! empty($options['decode_content']) - && $options['decode_content'] !== true - ) { - // Ensure that we don't have the header in different case and set the new value. - $options['_conditional'] = Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']); - $modify['set_headers']['Accept-Encoding'] = $options['decode_content']; - } - - if (isset($options['body'])) { - if (\is_array($options['body'])) { - throw $this->invalidBody(); - } - $modify['body'] = Psr7\Utils::streamFor($options['body']); - unset($options['body']); - } - - if (! empty($options['auth']) && \is_array($options['auth'])) { - $value = $options['auth']; - $type = isset($value[2]) ? \strtolower($value[2]) : 'basic'; - switch ($type) { - case 'basic': - // Ensure that we don't have the header in different case and set the new value. - $modify['set_headers'] = Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']); - $modify['set_headers']['Authorization'] = 'Basic ' - . \base64_encode("{$value[0]}:{$value[1]}"); - break; - case 'digest': - // @todo: Do not rely on curl - $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST; - $options['curl'][\CURLOPT_USERPWD] = "{$value[0]}:{$value[1]}"; - break; - case 'ntlm': - $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM; - $options['curl'][\CURLOPT_USERPWD] = "{$value[0]}:{$value[1]}"; - break; - } - } - - if (isset($options['query'])) { - $value = $options['query']; - if (\is_array($value)) { - $value = \http_build_query($value, '', '&', \PHP_QUERY_RFC3986); - } - if (! \is_string($value)) { - throw new InvalidArgumentException('query must be a string or array'); - } - $modify['query'] = $value; - unset($options['query']); - } - - // Ensure that sink is not an invalid value. - if (isset($options['sink'])) { - // TODO: Add more sink validation? - if (\is_bool($options['sink'])) { - throw new InvalidArgumentException('sink must not be a boolean'); - } - } - - $request = Psr7\Utils::modifyRequest($request, $modify); - if ($request->getBody() instanceof Psr7\MultipartStream) { - // Use a multipart/form-data POST if a Content-Type is not set. - // Ensure that we don't have the header in different case and set the new value. - $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); - $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' - . $request->getBody()->getBoundary(); - } - - // Merge in conditional headers if they are not present. - if (isset($options['_conditional'])) { - // Build up the changes so it's in a single clone of the message. - $modify = []; - foreach ($options['_conditional'] as $k => $v) { - if (! $request->hasHeader($k)) { - $modify['set_headers'][$k] = $v; - } - } - $request = Psr7\Utils::modifyRequest($request, $modify); - // Don't pass this internal value along to middleware/handlers. - unset($options['_conditional']); - } - - return $request; - } - - /** - * Return an InvalidArgumentException with pre-set message. - */ - private function invalidBody(): InvalidArgumentException - { - return new InvalidArgumentException('Passing in the "body" request ' - . 'option as an array to send a request is not supported. ' - . 'Please use the "form_params" request option to send a ' - . 'application/x-www-form-urlencoded request, or the "multipart" ' - . 'request option to send a multipart/form-data request.'); - } -} diff --git a/composer.json b/composer.json index 9cb412b..7673da2 100644 --- a/composer.json +++ b/composer.json @@ -8,13 +8,13 @@ "email": "huangdijia@gmail.com" }], "require": { - "php": ">=7.0", + "php": ">=7.1", "guzzlehttp/guzzle": "^6.0|^7.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", "mockery/mockery": "^1.0", - "phpstan/phpstan": "^0.12", + "phpstan/phpstan": "^1.0", "phpunit/phpunit": "^7.0|^8.0|^9.0|^10.0" }, "autoload": { @@ -22,7 +22,7 @@ "FriendsOfHyperf\\Jet\\": "src/" }, "files": [ - "src/helpers.php" + "src/Functions.php" ] }, "autoload-dev": { @@ -31,7 +31,10 @@ } }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "ergebnis/composer-normalize": true + } }, "suggest": { "swoole": ">=4.6.0" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3b21407..5b4e2dd 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -27,5 +27,8 @@ + + + diff --git a/src/Client.php b/src/Client.php index 832c244..bedb535 100644 --- a/src/Client.php +++ b/src/Client.php @@ -2,18 +2,17 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet; use FriendsOfHyperf\Jet\Exception\RecvFailedException; use FriendsOfHyperf\Jet\Exception\ServerException; -use Throwable; class Client { @@ -35,8 +34,8 @@ public function __construct(Metadata $metadata) /** * @param string $name * @param array $arguments - * @throws Throwable * @return mixed + * @throws \Throwable */ public function __call($name, $arguments) { @@ -64,12 +63,12 @@ public function __call($name, $arguments) throw new RecvFailedException('Recv failed'); } - return with($packer->unpack($ret), function ($data) { + return with((array) $packer->unpack($ret), function ($data) use ($ret) { if (array_key_exists('result', $data)) { return $data['result']; } - throw new ServerException($data['error'] ?? []); + throw new ServerException($data['error'] ?? ['code' => 0, 'message' => 'Invalid data: ' . $ret]); }); }; diff --git a/src/ClientFactory.php b/src/ClientFactory.php index fb43fca..8691c6c 100644 --- a/src/ClientFactory.php +++ b/src/ClientFactory.php @@ -2,91 +2,76 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet; -use Exception; use FriendsOfHyperf\Jet\Contract\DataFormatterInterface; use FriendsOfHyperf\Jet\Contract\PackerInterface; use FriendsOfHyperf\Jet\Contract\PathGeneratorInterface; use FriendsOfHyperf\Jet\Contract\TransporterInterface; -use GuzzleHttp\ClientInterface; -use InvalidArgumentException; class ClientFactory { - /** - * User agent. - * @var string - */ - protected static $userAgent; - - /** - * Set user agent. - */ - public static function setUserAgent(string $userAgent): void - { - self::$userAgent = $userAgent; - } - - /** - * Get user agent. - */ - public static function getUserAgent(): string - { - return self::$userAgent ?: sprintf( - 'jet/%s php/%s guzzle/%s curl/%s', - Client::MAJOR_VERSION, - PHP_VERSION, - defined(ClientInterface::class . '::VERSION') ? constant(ClientInterface::class . '::VERSION') : constant(ClientInterface::class . '::MAJOR_VERSION'), - curl_version()['version'] - ); - } - /** * Create a client. * @param null|int|string|TransporterInterface $transporter transporter, protocol, timeout or null - * @throws InvalidArgumentException - * @throws Exception + * @throws \InvalidArgumentException + * @throws \Exception */ - public static function create(string $service, $transporter = null, ?PackerInterface $packer = null, ?DataFormatterInterface $dataFormatter = null, ?PathGeneratorInterface $pathGenerator = null, ?int $tries = null): Client - { - if (! $metadata = ServiceManager::get($service)) { - $metadata = new Metadata($service); + public static function create( + string $service, + $transporter = null, + ?PackerInterface $packer = null, + ?DataFormatterInterface $dataFormatter = null, + ?PathGeneratorInterface $pathGenerator = null, + ?int $tries = null + ): Client { + if ($metadata = ServiceManager::get($service)) { + return new Client($metadata); + } + + if ( + func_num_args() == 2 + && is_string($transporter) + && $metadata = MetadataManager::get($transporter) + ) { + return new Client($metadata->withName($service)); + } + + $metadata = new Metadata($service); - if (RegistryManager::isRegistered(RegistryManager::DEFAULT)) { - $metadata->setRegistry(RegistryManager::get(RegistryManager::DEFAULT)); - } + if (RegistryManager::isRegistered(RegistryManager::DEFAULT)) { + $metadata = $metadata->withRegistry(RegistryManager::get(RegistryManager::DEFAULT)); + } - if ($transporter instanceof TransporterInterface) { - $metadata->setTransporter($transporter); - } elseif (is_numeric($transporter)) { - $metadata->setTimeout($transporter); - } elseif (is_string($transporter)) { - $metadata->setProtocol($transporter); - } + if ($transporter instanceof TransporterInterface) { + $metadata = $metadata->withTransporter($transporter); + } elseif (is_numeric($transporter)) { + $metadata = $metadata->withTimeout($transporter); + } elseif (is_string($transporter)) { + $metadata = $metadata->withProtocol($transporter); + } - if ($packer) { - $metadata->setPacker($packer); - } + if ($packer) { + $metadata = $metadata->withPacker($packer); + } - if ($dataFormatter) { - $metadata->setDataFormatter($dataFormatter); - } + if ($dataFormatter) { + $metadata = $metadata->withDataFormatter($dataFormatter); + } - if ($pathGenerator) { - $metadata->setPathGenerator($pathGenerator); - } + if ($pathGenerator) { + $metadata = $metadata->withPathGenerator($pathGenerator); + } - if ($tries) { - $metadata->setTries($tries); - } + if ($tries) { + $metadata = $metadata->withTries($tries); } return new Client($metadata); diff --git a/src/Consul/Agent.php b/src/Consul/Agent.php index 88d4196..6bedc1f 100644 --- a/src/Consul/Agent.php +++ b/src/Consul/Agent.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Consul; class Agent extends Client diff --git a/src/Consul/Catalog.php b/src/Consul/Catalog.php index 4f73393..272d3f6 100644 --- a/src/Consul/Catalog.php +++ b/src/Consul/Catalog.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Consul; use FriendsOfHyperf\Jet\Exception\ClientException; @@ -18,10 +18,10 @@ class Catalog extends Client { /** + * @return Response * @throws ServerException * @throws ClientException * @throws GuzzleException - * @return Response */ public function services(array $options = []) { diff --git a/src/Consul/Client.php b/src/Consul/Client.php index 07cfc40..73e76a2 100644 --- a/src/Consul/Client.php +++ b/src/Consul/Client.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Consul; use FriendsOfHyperf\Jet\Exception\ClientException; diff --git a/src/Consul/Health.php b/src/Consul/Health.php index 44c649b..505c80e 100644 --- a/src/Consul/Health.php +++ b/src/Consul/Health.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Consul; class Health extends Client diff --git a/src/Consul/Response.php b/src/Consul/Response.php index 89b145a..6b05760 100644 --- a/src/Consul/Response.php +++ b/src/Consul/Response.php @@ -2,16 +2,17 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Consul; use FriendsOfHyperf\Jet\Exception\ServerException; +use FriendsOfHyperf\Jet\Support\Arr; use Psr\Http\Message\ResponseInterface; class Response @@ -38,10 +39,10 @@ public function __call($name, $arguments) /** * @param null|mixed $default - * @throws ServerException * @return mixed + * @throws ServerException */ - public function json(string $key = null, $default = null) + public function json(?string $key = null, $default = null) { if (is_null($this->decoded)) { if ($this->response->getHeaderLine('Content-Type') !== 'application/json') { @@ -55,7 +56,7 @@ public function json(string $key = null, $default = null) return $this->decoded; } - return array_get($this->decoded, $key, $default); + return Arr::get($this->decoded, $key, $default); } /** diff --git a/src/Contract/DataFormatterInterface.php b/src/Contract/DataFormatterInterface.php index 61c1df7..b980481 100644 --- a/src/Contract/DataFormatterInterface.php +++ b/src/Contract/DataFormatterInterface.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Contract; interface DataFormatterInterface diff --git a/src/Contract/LoadBalancerInterface.php b/src/Contract/LoadBalancerInterface.php index 6137920..5008c7a 100644 --- a/src/Contract/LoadBalancerInterface.php +++ b/src/Contract/LoadBalancerInterface.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Contract; use FriendsOfHyperf\Jet\LoadBalancer\Node; diff --git a/src/Contract/PackerInterface.php b/src/Contract/PackerInterface.php index 7fe52a3..6aee968 100644 --- a/src/Contract/PackerInterface.php +++ b/src/Contract/PackerInterface.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Contract; interface PackerInterface diff --git a/src/Contract/PathGeneratorInterface.php b/src/Contract/PathGeneratorInterface.php index 96cefb1..c7be033 100644 --- a/src/Contract/PathGeneratorInterface.php +++ b/src/Contract/PathGeneratorInterface.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Contract; interface PathGeneratorInterface diff --git a/src/Contract/RegistryInterface.php b/src/Contract/RegistryInterface.php index 65a2ca4..ea779be 100644 --- a/src/Contract/RegistryInterface.php +++ b/src/Contract/RegistryInterface.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Contract; use FriendsOfHyperf\Jet\LoadBalancer\Node; diff --git a/src/Contract/TransporterInterface.php b/src/Contract/TransporterInterface.php index 728c3e7..750d14c 100644 --- a/src/Contract/TransporterInterface.php +++ b/src/Contract/TransporterInterface.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Contract; interface TransporterInterface diff --git a/src/DataFormatter/DataFormatter.php b/src/DataFormatter/DataFormatter.php index 402bccd..343e74e 100644 --- a/src/DataFormatter/DataFormatter.php +++ b/src/DataFormatter/DataFormatter.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\DataFormatter; use FriendsOfHyperf\Jet\Contract\DataFormatterInterface; diff --git a/src/DataFormatter/MultiplexDataFormatter.php b/src/DataFormatter/MultiplexDataFormatter.php new file mode 100644 index 0000000..fe470ed --- /dev/null +++ b/src/DataFormatter/MultiplexDataFormatter.php @@ -0,0 +1,73 @@ + $id, + 'path' => $path, + 'data' => $params, + 'extra' => [], + 'context' => [], + ]; + } + + /** + * @param array $data + */ + public function formatResponse($data): array + { + [$id, $result] = $data; + + return [ + 'id' => $id, + 'result' => $result, + 'context' => [], + ]; + } + + /** + * @param array $data + */ + public function formatErrorResponse($data): array + { + [$id, $code, $message, $data] = $data; + + if (isset($data) && $data instanceof \Throwable) { + $data = [ + 'class' => get_class($data), + 'code' => $data->getCode(), + 'message' => $data->getMessage(), + ]; + } + + return [ + 'id' => $id, + 'error' => [ + 'code' => $code, + 'message' => $message, + 'data' => $data, + ], + 'context' => [], + ]; + } +} diff --git a/src/Exception/ClientException.php b/src/Exception/ClientException.php index 9c5ef4d..002868e 100644 --- a/src/Exception/ClientException.php +++ b/src/Exception/ClientException.php @@ -2,15 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Exception; -class ClientException extends JetException -{ -} +class ClientException extends JetException {} diff --git a/src/Exception/ConnectionException.php b/src/Exception/ConnectionException.php index 79eb5ae..9a70138 100644 --- a/src/Exception/ConnectionException.php +++ b/src/Exception/ConnectionException.php @@ -2,15 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Exception; -class ConnectionException extends JetException -{ -} +class ConnectionException extends JetException {} diff --git a/src/Exception/ExceptionThrower.php b/src/Exception/ExceptionThrower.php index 1e81b9a..56e13b0 100644 --- a/src/Exception/ExceptionThrower.php +++ b/src/Exception/ExceptionThrower.php @@ -2,30 +2,28 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ -namespace FriendsOfHyperf\Jet\Exception; -use Throwable; +namespace FriendsOfHyperf\Jet\Exception; final class ExceptionThrower { /** - * @var Throwable + * @var \Throwable */ private $throwable; - public function __construct(Throwable $throwable) + public function __construct(\Throwable $throwable) { $this->throwable = $throwable; } - public function getThrowable(): Throwable + public function getThrowable(): \Throwable { return $this->throwable; } diff --git a/src/Exception/JetException.php b/src/Exception/JetException.php index 94f6b8f..d58779e 100644 --- a/src/Exception/JetException.php +++ b/src/Exception/JetException.php @@ -2,17 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ -namespace FriendsOfHyperf\Jet\Exception; -use RuntimeException; +namespace FriendsOfHyperf\Jet\Exception; -class JetException extends RuntimeException -{ -} +class JetException extends \RuntimeException {} diff --git a/src/Exception/NoNodesAvailableException.php b/src/Exception/NoNodesAvailableException.php new file mode 100644 index 0000000..6e71a78 --- /dev/null +++ b/src/Exception/NoNodesAvailableException.php @@ -0,0 +1,14 @@ +|\Throwable $exception + * @return TValue + * @throws \Throwable + */ +function throw_if($condition, $exception, ...$parameters) +{ + if ($condition) { + throw is_string($exception) ? new $exception(...$parameters) : $exception; + } + + return $condition; +} + +/** + * @template TValue + * + * @param TValue $value + * @return TValue + */ +function tap($value, ?callable $callback = null) +{ + if (is_null($callback)) { + return new class($value) { + public $target; + + public function __construct($target) + { + $this->target = $target; + } + + public function __call($method, $parameters) + { + $this->target->{$method}(...$parameters); + + return $this->target; + } + }; + } + + $callback($value); + + return $value; +} + +/** + * @template TValue + * @template TReturn + * + * @param TValue $value + * @param null|(callable(TValue):TReturn) $callback + * @return ($callback is null ? TValue : TReturn) + */ +function with($value, ?callable $callback = null) // @phpstan-ignore-line +{ + return is_null($callback) ? $value : $callback($value); +} + +/** + * @param mixed $value + * @return mixed + */ +function value($value) +{ + return $value instanceof \Closure ? $value() : $value; +} diff --git a/src/LoadBalancer/AbstractLoadBalancer.php b/src/LoadBalancer/AbstractLoadBalancer.php index 9d1a752..2455498 100644 --- a/src/LoadBalancer/AbstractLoadBalancer.php +++ b/src/LoadBalancer/AbstractLoadBalancer.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\LoadBalancer; use FriendsOfHyperf\Jet\Contract\LoadBalancerInterface; diff --git a/src/LoadBalancer/Node.php b/src/LoadBalancer/Node.php index 06119fe..240acf2 100644 --- a/src/LoadBalancer/Node.php +++ b/src/LoadBalancer/Node.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\LoadBalancer; class Node diff --git a/src/LoadBalancer/Random.php b/src/LoadBalancer/Random.php index 9fcc25f..763c0e1 100644 --- a/src/LoadBalancer/Random.php +++ b/src/LoadBalancer/Random.php @@ -2,15 +2,17 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\LoadBalancer; +use FriendsOfHyperf\Jet\Exception\NoNodesAvailableException; + class Random extends AbstractLoadBalancer { /** @@ -19,7 +21,7 @@ class Random extends AbstractLoadBalancer public function select(): Node { if (empty($this->nodes)) { - throw new \RuntimeException('Cannot select any node from load balancer.'); + throw new NoNodesAvailableException('Cannot select any node from load balancer.'); } $key = array_rand($this->nodes); diff --git a/src/LoadBalancer/RoundRobin.php b/src/LoadBalancer/RoundRobin.php index beb1d35..ab0cfd7 100644 --- a/src/LoadBalancer/RoundRobin.php +++ b/src/LoadBalancer/RoundRobin.php @@ -2,16 +2,16 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\LoadBalancer; -use RuntimeException; +use FriendsOfHyperf\Jet\Exception\NoNodesAvailableException; class RoundRobin extends AbstractLoadBalancer { @@ -28,7 +28,7 @@ public function select(): Node $count = count($this->nodes); if ($count <= 0) { - throw new RuntimeException('Nodes missing.'); + throw new NoNodesAvailableException('Nodes missing.'); } $item = $this->nodes[self::$current % $count]; diff --git a/src/Metadata.php b/src/Metadata.php index 62baba6..9113e49 100644 --- a/src/Metadata.php +++ b/src/Metadata.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet; use FriendsOfHyperf\Jet\Contract\DataFormatterInterface; @@ -19,7 +19,6 @@ use FriendsOfHyperf\Jet\DataFormatter\DataFormatter; use FriendsOfHyperf\Jet\Packer\JsonEofPacker; use FriendsOfHyperf\Jet\PathGenerator\PathGenerator; -use RuntimeException; class Metadata { @@ -68,11 +67,22 @@ class Metadata */ protected $timeout = 3; - public function __construct(string $name) + public function __construct(string $name = '') { $this->name = $name; } + /** + * @return static + */ + public function withName(string $name) + { + $clone = clone $this; + $clone->name = $name; + + return $clone; + } + /** * Get name. * @return string @@ -82,12 +92,27 @@ public function getName() return $this->name; } + /** + * @return static + */ + public function withProtocol(string $protocol) + { + $clone = clone $this; + $clone->protocol = $protocol; + + return $clone; + } + /** * Set protocol. + * @deprecated use withProtocol instead, will be removed in v4.0 + * @return $this */ public function setProtocol(string $protocol) { $this->protocol = $protocol; + + return $this; } /** @@ -99,12 +124,27 @@ public function getProtocol() return $this->protocol; } + /** + * @return static + */ + public function withTransporter(TransporterInterface $transporter) + { + $clone = clone $this; + $clone->transporter = $transporter; + + return $clone; + } + /** * Set transporter. + * @deprecated use withTransporter instead, will be removed in v4.0 + * @return $this */ public function setTransporter(TransporterInterface $transporter) { $this->transporter = $transporter; + + return $this; } /** @@ -121,15 +161,30 @@ public function getTransporter() return $this->registry->getTransporter($this->name, $this->protocol, $this->timeout); } - throw new RuntimeException('Transporter not registered yet.'); + throw new \RuntimeException('Transporter not registered yet.'); + } + + /** + * @return static + */ + public function withPacker(PackerInterface $packer) + { + $clone = clone $this; + $clone->packer = $packer; + + return $clone; } /** * Set packer. + * @deprecated use withPacker instead, will be removed in v4.0 + * @return $this */ public function setPacker(PackerInterface $packer) { $this->packer = $packer; + + return $this; } /** @@ -145,12 +200,27 @@ public function getPacker() return $this->packer; } + /** + * @return static + */ + public function withDataFormatter(DataFormatterInterface $dataFormatter) + { + $clone = clone $this; + $clone->dataFormatter = $dataFormatter; + + return $clone; + } + /** * Set data formatter. + * @deprecated use withDataFormatter instead, will be removed in v4.0 + * @return $this */ public function setDataFormatter(DataFormatterInterface $dataFormatter) { $this->dataFormatter = $dataFormatter; + + return $this; } /** @@ -166,12 +236,26 @@ public function getDataFormatter() return $this->dataFormatter; } + /** + * @return static + */ + public function withPathGenerator(PathGeneratorInterface $pathGenerator) + { + $clone = clone $this; + $clone->pathGenerator = $pathGenerator; + + return $clone; + } + /** * Set path generator. + * @return $this */ public function setPathGenerator(PathGeneratorInterface $pathGenerator) { $this->pathGenerator = $pathGenerator; + + return $this; } /** @@ -187,12 +271,27 @@ public function getPathGenerator() return $this->pathGenerator; } + /** + * @return static + */ + public function withRegistry(RegistryInterface $registry) + { + $clone = clone $this; + $clone->registry = $registry; + + return $clone; + } + /** * Set registry. + * @deprecated use withRegistry instead, will be removed in v4.0 + * @return $this */ public function setRegistry(RegistryInterface $registry) { $this->registry = $registry; + + return $this; } /** @@ -204,12 +303,26 @@ public function getRegistry() return $this->registry; } + /** + * @return static + */ + public function withTries(int $tries) + { + $clone = clone $this; + $clone->tries = $tries; + + return $clone; + } + /** * Set tries. + * @return $this */ public function setTries(int $tries) { $this->tries = $tries; + + return $this; } /** @@ -221,12 +334,26 @@ public function getTries() return (int) $this->tries; } + /** + * @return static + */ + public function withTimeout(int $timeout) + { + $clone = clone $this; + $clone->timeout = $timeout; + + return $clone; + } + /** * Set timeout. + * @return $this */ public function setTimeout(int $timeout) { $this->timeout = $timeout; + + return $this; } /** diff --git a/src/MetadataManager.php b/src/MetadataManager.php new file mode 100644 index 0000000..d86ae69 --- /dev/null +++ b/src/MetadataManager.php @@ -0,0 +1,33 @@ + + */ + protected static $metadata = []; + + public static function register(string $name, Metadata $metadata) + { + static::$metadata[$name] = $metadata; + } + + /** + * @return null|Metadata + */ + public static function get(string $name) + { + return isset(static::$metadata[$name]) ? clone static::$metadata[$name] : null; + } +} diff --git a/src/Packer/JsonEofPacker.php b/src/Packer/JsonEofPacker.php index 9d7d05c..2c144d0 100644 --- a/src/Packer/JsonEofPacker.php +++ b/src/Packer/JsonEofPacker.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Packer; use FriendsOfHyperf\Jet\Contract\PackerInterface; diff --git a/src/Packer/JsonLengthPacker.php b/src/Packer/JsonLengthPacker.php index 5ef9af0..7062241 100644 --- a/src/Packer/JsonLengthPacker.php +++ b/src/Packer/JsonLengthPacker.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Packer; use FriendsOfHyperf\Jet\Contract\PackerInterface; diff --git a/src/Packer/JsonMultiplexPacker.php b/src/Packer/JsonMultiplexPacker.php new file mode 100644 index 0000000..328b54d --- /dev/null +++ b/src/Packer/JsonMultiplexPacker.php @@ -0,0 +1,41 @@ + array_get($node, 'Checks.1.Type'), - 'protocol' => array_get($node, 'Service.Meta.Protocol'), + 'type' => Arr::get($node, 'Checks.1.Type'), + 'protocol' => Arr::get($node, 'Service.Meta.Protocol'), ] ); } @@ -146,7 +149,7 @@ public function getTransporter(string $service, ?string $protocol = null, int $t $nodes = $this->getServiceNodes($service, $protocol); if (count($nodes) <= 0) { - throw new RuntimeException('Service nodes not found!'); + throw new \RuntimeException('Service nodes not found!'); } $serviceBalancer = new RoundRobin($nodes); diff --git a/src/RegistryManager.php b/src/RegistryManager.php index d22e9ad..2393129 100644 --- a/src/RegistryManager.php +++ b/src/RegistryManager.php @@ -2,18 +2,17 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet; use FriendsOfHyperf\Jet\Contract\RegistryInterface; use FriendsOfHyperf\Jet\Exception\JetException; -use InvalidArgumentException; class RegistryManager { @@ -36,13 +35,13 @@ public static function get($name = self::DEFAULT) /** * @param string $name * @param RegistryInterface $registry - * @throws InvalidArgumentException + * @throws \InvalidArgumentException * @throws JetException */ public static function register($name, $registry, bool $force = false) { - if (! ($registry instanceof RegistryInterface)) { - throw new InvalidArgumentException('$registry must be instanceof RegistryInterface'); + if (! $registry instanceof RegistryInterface) { + throw new \InvalidArgumentException('$registry must be instanceof RegistryInterface'); } if (! $force && self::isRegistered($name)) { diff --git a/src/ServiceManager.php b/src/ServiceManager.php index 9bc4057..7363d93 100644 --- a/src/ServiceManager.php +++ b/src/ServiceManager.php @@ -2,16 +2,14 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ -namespace FriendsOfHyperf\Jet; -use InvalidArgumentException; +namespace FriendsOfHyperf\Jet; class ServiceManager { @@ -37,7 +35,7 @@ public static function isRegistered(string $service) } /** - * @throws InvalidArgumentException + * @throws \InvalidArgumentException */ public static function register(string $service, Metadata $metadata) { diff --git a/src/Support/Arr.php b/src/Support/Arr.php new file mode 100644 index 0000000..afc1d8e --- /dev/null +++ b/src/Support/Arr.php @@ -0,0 +1,111 @@ +offsetExists($key); + } + + return array_key_exists($key, $array); + } + + /** + * @param mixed $value + * @return bool + */ + public static function accessible($value) + { + return is_array($value) || $value instanceof \ArrayAccess; + } +} diff --git a/src/Support/Str.php b/src/Support/Str.php new file mode 100644 index 0000000..e17bb4b --- /dev/null +++ b/src/Support/Str.php @@ -0,0 +1,87 @@ + $replace + * @param string $subject + * @return string + */ + public static function replaceArray($search, $replace, $subject) + { + foreach ($replace as $value) { + $subject = self::replaceFirst($search, $value, $subject); + } + + return $subject; + } +} diff --git a/src/Support/UserAgent.php b/src/Support/UserAgent.php new file mode 100644 index 0000000..89a8a0b --- /dev/null +++ b/src/Support/UserAgent.php @@ -0,0 +1,39 @@ +host = $host; - $this->port = $port; - $this->config = array_merge_recursive($config, [ - 'headers' => [ - 'Content-Type' => 'application/json', - 'X-Real-Ip' => $_SERVER['SERVER_ADDR'] ?? '', - 'X-Forwarded-For' => $_SERVER['REMOTE_ADDR'] ?? '', - 'User-Agent' => ClientFactory::getUserAgent(), - ], + parent::__construct($host, $port); + + $this->config = array_replace([ 'http_errors' => false, - ]); + 'timeout' => $this->timeout, + ], $config); + $this->config['headers'] = array_replace([ + 'Content-Type' => 'application/json', + 'X-Real-Ip' => $_SERVER['SERVER_ADDR'] ?? '', + 'X-Forwarded-For' => $_SERVER['REMOTE_ADDR'] ?? '', + 'User-Agent' => UserAgent::get(), + ], $config['headers'] ?? []); } public function send(string $data) diff --git a/src/Transporter/MultiplexRpcTransporter.php b/src/Transporter/MultiplexRpcTransporter.php new file mode 100644 index 0000000..5b89b04 --- /dev/null +++ b/src/Transporter/MultiplexRpcTransporter.php @@ -0,0 +1,84 @@ +client, false); + + while (true) { + $header = $this->readBytes(4); + + $unpacked = unpack('Nlength', $header); + $length = $unpacked['length']; + + if ($length < 4) { + throw new RecvFailedException(sprintf('Invalid package length: %d', $length)); + } + $body = $this->readBytes($length); + if (in_array($body, [self::PING, self::PONG], true)) { + continue; + } + + return $header . $body; + } + } + + /** + * @throws \Exception + */ + private function readBytes(int $length): string + { + $buffer = ''; + + while (strlen($buffer) < $length) { + $read = [$this->client]; + $write = null; + $except = null; + + $selected = stream_select($read, $write, $except, $this->timeout); + if ($selected === false) { + throw new \RuntimeException('Failed to select stream.'); + } + + if ($selected === 0) { + throw new RecvFailedException('Receive timeout.'); + } + + foreach ($read as $stream) { + /** @var false|string $chunk */ + $chunk = fread($stream, $length - strlen($buffer)); + + if ($chunk === false) { + throw new RecvFailedException('Receive failed.'); + } + + if ($chunk === '' && feof($stream)) { + throw new ConnectionException('Connection was closed.'); + } + + $buffer .= $chunk; + } + } + + return $buffer; + } +} diff --git a/src/Transporter/StreamSocketTransporter.php b/src/Transporter/StreamSocketTransporter.php index bd3c68e..76e98af 100644 --- a/src/Transporter/StreamSocketTransporter.php +++ b/src/Transporter/StreamSocketTransporter.php @@ -2,22 +2,20 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Transporter; -use Exception; use FriendsOfHyperf\Jet\Exception\ConnectionException; use FriendsOfHyperf\Jet\Exception\ExceptionThrower; use FriendsOfHyperf\Jet\Exception\RecvFailedException; -use InvalidArgumentException; -use RuntimeException; -use Throwable; + +use function FriendsOfHyperf\Jet\retry; class StreamSocketTransporter extends AbstractTransporter { @@ -26,11 +24,6 @@ class StreamSocketTransporter extends AbstractTransporter */ protected $client; - /** - * @var float - */ - protected $timeout; - /** * @var bool */ @@ -42,8 +35,8 @@ public function __destruct() } /** - * @throws InvalidArgumentException - * @throws RuntimeException + * @throws \InvalidArgumentException + * @throws \RuntimeException */ public function send(string $data) { @@ -52,14 +45,14 @@ public function send(string $data) } /** - * @throws Throwable * @return string + * @throws \Throwable */ public function recv() { try { return $this->receive(); - } catch (Throwable $e) { + } catch (\Throwable $e) { $this->close(); throw $e; } @@ -71,19 +64,19 @@ public function recv() public function receive() { $buf = ''; - $timeout = 1000; + $timeoutMs = $this->timeout > 0 ? $this->timeout * 1000 : 1000; stream_set_blocking($this->client, false); // The maximum number of retries is 12, and 1000 microseconds is the minimum waiting time. // The waiting time is doubled each time until the server writes data to the buffer. // Usually, the data can be obtained within 1 microsecond. - $result = retry(12, function () use (&$buf, &$timeout) { + $result = retry(12, function () use (&$buf, &$timeoutMs) { $read = [$this->client]; $write = null; $except = null; - while (stream_select($read, $write, $except, 0, $timeout)) { + while (stream_select($read, $write, $except, 0, $timeoutMs)) { foreach ($read as $r) { $res = fread($r, 8192); if (feof($r)) { @@ -94,7 +87,7 @@ public function receive() } if (! $buf) { - $timeout *= 2; + $timeoutMs *= 2; throw new RecvFailedException('No data was received'); } @@ -110,9 +103,9 @@ public function receive() } /** - * @throws InvalidArgumentException - * @throws Exception - * @return (string|int)[] + * @return array{string, int} + * @throws \InvalidArgumentException + * @throws \Exception */ protected function getTarget() { @@ -123,36 +116,39 @@ protected function getTarget() } if (! $node->host || ! $node->port) { - throw new InvalidArgumentException(sprintf('Invalid host %s or port %s.', $node->host, $node->port)); + throw new \InvalidArgumentException(sprintf('Invalid host %s or port %s.', $node->host, $node->port)); } return [$node->host, $node->port]; } /** - * @throws InvalidArgumentException - * @throws Exception + * @throws \InvalidArgumentException + * @throws \Exception */ protected function connect() { if ($this->isConnected) { return; } + if ($this->client) { fclose($this->client); unset($this->client); } - [$host, $port] = $this->getTarget(); + retry(5, function() { + [$host, $port] = $this->getTarget(); - $client = stream_socket_client("tcp://{$host}:{$port}", $errno, $errstr, $this->timeout); + $client = stream_socket_client("tcp://{$host}:{$port}", $errno, $errstr, $this->timeout); - if ($client === false) { - throw new ConnectionException(sprintf('[%d] %s', $errno, $errstr)); - } + if ($client === false) { + throw new ConnectionException(sprintf('[%d] %s', $errno, $errstr)); + } - $this->client = $client; - $this->isConnected = true; + $this->client = $client; + $this->isConnected = true; + }); } protected function close() diff --git a/src/helpers.php b/src/helpers.php deleted file mode 100644 index 367fb96..0000000 --- a/src/helpers.php +++ /dev/null @@ -1,283 +0,0 @@ -target = $target; - } - - public function __call($method, $parameters) - { - $this->target->{$method}(...$parameters); - - return $this->target; - } - }; - } - - $callback($value); - - return $value; - } -} - -if (! function_exists('with')) { - /** - * @param mixed $value - * @return mixed - */ - function with($value, callable $callback = null) - { - return is_null($callback) ? $value : $callback($value); - } -} - -if (! function_exists('str_snake')) { - /** - * @param string $delimiter - * @return string - */ - function str_snake(string $value, $delimiter = '_') - { - if (! ctype_lower($value)) { - $value = preg_replace('/\s+/u', '', ucwords($value)); - $value = str_lower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value)); - } - - return $value; - } -} - -if (! function_exists('str_lower')) { - /** - * @return string - */ - function str_lower(string $value) - { - return mb_strtolower($value, 'UTF-8'); - } -} - -if (! function_exists('str_studly')) { - /** - * @return string - */ - function str_studly(string $value, string $gap = '') - { - $value = ucwords(str_replace(['-', '_'], ' ', $value)); - - return str_replace(' ', $gap, $value); - } -} - -if (! function_exists('str_replace_first')) { - /** - * @return string - */ - function str_replace_first(string $search, string $replace, string $subject) - { - if ($search == '') { - return $subject; - } - - $position = strpos($subject, $search); - - if ($position !== false) { - return substr_replace($subject, $replace, $position, strlen($search)); - } - - return $subject; - } -} - -if (! function_exists('str_replace_array')) { - /** - * @return string - */ - function str_replace_array(string $search, array $replace, string $subject) - { - foreach ($replace as $value) { - $subject = str_replace_first($search, (string) $value, $subject); - } - - return $subject; - } -} - -if (! function_exists('value')) { - /** - * @param mixed $value - * @return mixed - */ - function value($value) - { - return $value instanceof Closure ? $value() : $value; - } -} - -if (! function_exists('array_get')) { - /** - * Get an item from an array using "dot" notation. - * - * @param array|\ArrayAccess $array - * @param null|int|string $key - * @param mixed $default - */ - function array_get($array, $key = null, $default = null) - { - if (is_null($key)) { - return $array; - } - - if (isset($array[$key])) { - return $array[$key]; - } - - if (! is_string($key) || strpos($key, '.') === false) { - return $array[$key] ?? value($default); - } - - foreach (explode('.', $key) as $segment) { - if (array_accessible($array) && array_exists($array, $segment)) { - $array = $array[$segment]; - } else { - return value($default); - } - } - - return $array; - } -} - -if (! function_exists('array_has')) { - /** - * Check if an item or items exist in an array using "dot" notation. - * - * @param array|\ArrayAccess $array - * @param null|array|string $keys - */ - function array_has($array, $keys) - { - if (is_null($keys)) { - return false; - } - - $keys = (array) $keys; - - if (! $array || $keys === []) { - return false; - } - - foreach ($keys as $key) { - $subKeyArray = $array; - - if (array_exists($array, $key)) { - continue; - } - - foreach (explode('.', $key) as $segment) { - if (array_accessible($subKeyArray) && array_exists($subKeyArray, $segment)) { - $subKeyArray = $subKeyArray[$segment]; - } else { - return false; - } - } - } - - return true; - } -} - -if (! function_exists('array_exists')) { - /** - * @param mixed $array - * @param int|string $key - * @return bool - */ - function array_exists($array, $key) - { - if ($array instanceof ArrayAccess) { - return $array->offsetExists($key); - } - - return array_key_exists($key, $array); - } -} - -if (! function_exists('array_accessible')) { - /** - * @param mixed $value - * @return bool - */ - function array_accessible($value) - { - return is_array($value) || $value instanceof ArrayAccess; - } -} diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 25c3251..2c1ada5 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -2,16 +2,20 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Tests; use FriendsOfHyperf\Jet\ClientFactory; +use FriendsOfHyperf\Jet\DataFormatter\MultiplexDataFormatter; +use FriendsOfHyperf\Jet\Metadata; +use FriendsOfHyperf\Jet\MetadataManager; +use FriendsOfHyperf\Jet\Packer\JsonMultiplexPacker; use FriendsOfHyperf\Jet\RegistryManager; /** @@ -62,4 +66,34 @@ public function testCalculatorServiceByStreamSocketTransporter() $this->assertSame($a + $b, $client->add($a, $b)); } + + public function testCalculatorServiceByMultiplexRpcTransporter() + { + $client = ClientFactory::create( + $this->service, + $this->createMultiplexRpcTransporter(), + new JsonMultiplexPacker(), + new MultiplexDataFormatter() + ); + + $a = rand(1, 99); + $b = rand(1, 99); + + $this->assertSame($a + $b, $client->add($a, $b)); + } + + public function testMetadataManager() + { + MetadataManager::register( + $name = 'test', + (new Metadata())->withTransporter($this->createGuzzleHttpTransporter()) + ); + + $client = ClientFactory::create($this->service, $name); + + $a = rand(1, 99); + $b = rand(1, 99); + + $this->assertSame($a + $b, $client->add($a, $b)); + } } diff --git a/tests/RegistryTest.php b/tests/RegistryTest.php index 9862c5e..0e7fb26 100644 --- a/tests/RegistryTest.php +++ b/tests/RegistryTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Tests; use FriendsOfHyperf\Jet\Contract\RegistryInterface; diff --git a/tests/TestCase.php b/tests/TestCase.php index a7aaf55..2c625e0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,17 +2,18 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ + namespace FriendsOfHyperf\Jet\Tests; use FriendsOfHyperf\Jet\Registry\ConsulRegistry; use FriendsOfHyperf\Jet\Transporter\GuzzleHttpTransporter; +use FriendsOfHyperf\Jet\Transporter\MultiplexRpcTransporter; use FriendsOfHyperf\Jet\Transporter\StreamSocketTransporter; /** @@ -37,6 +38,12 @@ class TestCase extends \PHPUnit\Framework\TestCase private $jsonrpcHttpTimeout; + private $jsonrpcLengthCheckHost; + + private $jsonrpcLengthCheckPort; + + private $jsonrpcLengthCheckTimeout; + public function __construct($name = null, array $data = [], $dataName = '') { parent::__construct($name, $data, $dataName); @@ -51,6 +58,10 @@ public function __construct($name = null, array $data = [], $dataName = '') $this->jsonrpcHttpHost = $_ENV['JSONRPC_HTTP_HOST'] ?? '127.0.0.1'; $this->jsonrpcHttpPort = (int) ($_ENV['JSONRPC_HTTP_PORT'] ?? 9502); $this->jsonrpcHttpTimeout = (int) ($_ENV['JSONRPC_HTTP_TIMEOUT'] ?? 2); + + $this->jsonrpcLengthCheckHost = $_ENV['JSONRPC_LENGTH_CHECK_HOST'] ?? '127.0.0.1'; + $this->jsonrpcLengthCheckPort = (int) ($_ENV['JSONRPC_LENGTH_CHECK_PORT'] ?? 9504); + $this->jsonrpcLengthCheckTimeout = (int) ($_ENV['JSONRPC_LENGTH_CHECK_TIMEOUT'] ?? 2); } public function createGuzzleHttpTransporter() @@ -63,6 +74,11 @@ public function createStreamSocketTransporter() return new StreamSocketTransporter($this->jsonrpcHost, $this->jsonrpcPort, $this->jsonrpcTimeout); } + public function createMultiplexRpcTransporter() + { + return new MultiplexRpcTransporter($this->jsonrpcLengthCheckHost, $this->jsonrpcLengthCheckPort, $this->jsonrpcLengthCheckTimeout); + } + protected function createRegistry() { return new ConsulRegistry(['uri' => $this->consulUri, 'timeout' => $this->consulTimeout]); diff --git a/tests/register.php b/tests/register.php index 88b009c..3ac119c 100644 --- a/tests/register.php +++ b/tests/register.php @@ -2,12 +2,11 @@ declare(strict_types=1); /** - * This file is part of jet. + * This file is part of friendsofhyperf/jet. * * @link https://github.com/friendsofhyperf/jet * @document https://github.com/friendsofhyperf/jet/blob/main/README.md * @contact huangdijia@gmail.com - * @license https://github.com/friendsofhyperf/jet/blob/main/LICENSE */ require_once __DIR__ . '/../vendor/autoload.php';