mirror of
https://gitee.com/hyperf/hyperf.git
synced 2024-12-02 03:37:44 +08:00
Merge branch 'master' into cache
This commit is contained in:
commit
a1d3e79b56
@ -4,6 +4,7 @@
|
||||
|
||||
- [#321](https://github.com/hyperf-cloud/hyperf/pull/321) Added custom object support for controller parameters in http-server.
|
||||
- [#324](https://github.com/hyperf-cloud/hyperf/pull/324) Added NodeRequestIdGenerator, an implementation of `Hyperf\Contract\IdGeneratorInterface`
|
||||
- [#336](https://github.com/hyperf-cloud/hyperf/pull/336) Added Proxy RPC Client.
|
||||
- [#346](https://github.com/hyperf-cloud/hyperf/pull/346) [#348](https://github.com/hyperf-cloud/hyperf/pull/348) Added filesystem driver for `hyperf/cache`.
|
||||
|
||||
## Changed
|
||||
@ -19,6 +20,7 @@
|
||||
- [#333](https://github.com/hyperf-cloud/hyperf/pull/333) Fixed Function Redis::delete() is deprecated.
|
||||
- [#334](https://github.com/hyperf-cloud/hyperf/pull/334) Fixed configuration of aliyun acm is not work expected.
|
||||
- [#337](https://github.com/hyperf-cloud/hyperf/pull/337) Fixed 500 response when key of header is not string.
|
||||
- [#338](https://github.com/hyperf-cloud/hyperf/pull/338) Fixed `ProviderConfig::load` will convert array when dependencies has the same key.
|
||||
- [#340](https://github.com/hyperf-cloud/hyperf/pull/340) Fixed function `make` not support index-based array as parameters.
|
||||
|
||||
# v1.0.9 - 2019-08-03
|
||||
|
@ -169,6 +169,7 @@
|
||||
"HyperfTest\\ConfigAliyunAcm\\": "src/config-aliyun-acm/tests/",
|
||||
"HyperfTest\\ConfigApollo\\": "src/config-apollo/tests/",
|
||||
"HyperfTest\\ConfigEtcd\\": "src/config-etcd/tests/",
|
||||
"HyperfTest\\Config\\": "src/config/tests/",
|
||||
"HyperfTest\\Constants\\": "src/constants/tests/",
|
||||
"HyperfTest\\Consul\\": "src/consul/tests/",
|
||||
"HyperfTest\\Crontab\\": "src/crontab/tests/",
|
||||
|
@ -13,6 +13,7 @@
|
||||
<directory suffix="Test.php">./src/amqp/tests</directory>
|
||||
<directory suffix="Test.php">./src/async-queue/tests</directory>
|
||||
<directory suffix="Test.php">./src/cache/tests</directory>
|
||||
<directory suffix="Test.php">./src/config/tests</directory>
|
||||
<directory suffix="Test.php">./src/constants/tests</directory>
|
||||
<directory suffix="Test.php">./src/consul/tests</directory>
|
||||
<directory suffix="Test.php">./src/database/tests</directory>
|
||||
|
1
src/config/.gitattributes
vendored
Normal file
1
src/config/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
/tests export-ignore
|
@ -44,6 +44,7 @@
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"HyperfTest\\Config\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
|
@ -35,23 +35,37 @@ class ProviderConfig
|
||||
public static function load(): array
|
||||
{
|
||||
if (! static::$privoderConfigs) {
|
||||
$config = [];
|
||||
$providers = Composer::getMergedExtra('hyperf')['config'];
|
||||
foreach ($providers ?? [] as $provider) {
|
||||
if (is_string($provider) && class_exists($provider) && method_exists($provider, '__invoke')) {
|
||||
$providerConfig = (new $provider())();
|
||||
$config = array_merge_recursive($config, $providerConfig);
|
||||
}
|
||||
}
|
||||
|
||||
static::$privoderConfigs = $config;
|
||||
unset($config, $providerConfig);
|
||||
$providers = Composer::getMergedExtra('hyperf')['config'] ?? [];
|
||||
static::$privoderConfigs = static::loadProviders($providers);
|
||||
}
|
||||
return static::$privoderConfigs;
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
public static function clear(): void
|
||||
{
|
||||
static::$privoderConfigs = [];
|
||||
}
|
||||
|
||||
protected static function loadProviders(array $providers): array
|
||||
{
|
||||
$providerConfigs = [];
|
||||
foreach ($providers ?? [] as $provider) {
|
||||
if (is_string($provider) && class_exists($provider) && method_exists($provider, '__invoke')) {
|
||||
$providerConfigs[] = (new $provider())();
|
||||
}
|
||||
}
|
||||
|
||||
return static::merge(...$providerConfigs);
|
||||
}
|
||||
|
||||
protected static function merge(...$arrays): array
|
||||
{
|
||||
$result = array_merge_recursive(...$arrays);
|
||||
if (isset($result['dependencies'])) {
|
||||
$dependencies = array_column($arrays, 'dependencies');
|
||||
$result['dependencies'] = array_merge(...$dependencies);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
124
src/config/tests/ProviderConfigTest.php
Normal file
124
src/config/tests/ProviderConfigTest.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace HyperfTest\Config;
|
||||
|
||||
use Hyperf\Utils\Arr;
|
||||
use HyperfTest\Config\Stub\FooConfigProvider;
|
||||
use HyperfTest\Config\Stub\ProviderConfig;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class ProviderConfigTest extends TestCase
|
||||
{
|
||||
public function testProviderConfigMerge()
|
||||
{
|
||||
$c1 = [
|
||||
'listeners' => ['L1'],
|
||||
'dependencies' => [
|
||||
'D1' => 'D1',
|
||||
'D2' => 'D2',
|
||||
],
|
||||
];
|
||||
|
||||
$c2 = [
|
||||
'listeners' => ['L2'],
|
||||
'dependencies' => [
|
||||
'D1' => 'D1',
|
||||
'D2' => 'D3',
|
||||
],
|
||||
];
|
||||
|
||||
$c3 = [
|
||||
'listeners' => ['L2'],
|
||||
'dependencies' => [
|
||||
'D1' => 'D1',
|
||||
'D3' => 'D3',
|
||||
'D4' => 'D4',
|
||||
],
|
||||
];
|
||||
|
||||
$result = ProviderConfig::merge($c1, $c2, $c3);
|
||||
|
||||
$this->assertSame(['D1' => 'D1', 'D2' => 'D3', 'D3' => 'D3', 'D4' => 'D4'], $result['dependencies']);
|
||||
}
|
||||
|
||||
public function testProviderConfigNotHaveDependencies()
|
||||
{
|
||||
$c1 = [
|
||||
'listeners' => ['L1'],
|
||||
'dependencies' => [
|
||||
'D1' => 'D1',
|
||||
'D2' => 'D2',
|
||||
],
|
||||
];
|
||||
|
||||
$c2 = [
|
||||
'listeners' => ['L2'],
|
||||
];
|
||||
|
||||
$result = ProviderConfig::merge($c1, $c2);
|
||||
$this->assertSame(['D1' => 'D1', 'D2' => 'D2'], $result['dependencies']);
|
||||
$this->assertSame(['L1', 'L2'], $result['listeners']);
|
||||
}
|
||||
|
||||
public function testProviderConfigHaveNull()
|
||||
{
|
||||
$c1 = [
|
||||
'listeners' => ['L1'],
|
||||
];
|
||||
|
||||
$c2 = [
|
||||
'listeners' => [value(function () {
|
||||
return null;
|
||||
})],
|
||||
];
|
||||
|
||||
$result = ProviderConfig::merge($c1, $c2);
|
||||
$this->assertSame(['L1', null], $result['listeners']);
|
||||
}
|
||||
|
||||
public function testProviderConfigLoadProviders()
|
||||
{
|
||||
$config = json_decode(file_get_contents(BASE_PATH . '/composer.json'), true);
|
||||
|
||||
$providers = $config['extra']['hyperf']['config'];
|
||||
|
||||
$res = ProviderConfig::loadProviders($providers);
|
||||
|
||||
$dependencies = $res['dependencies'];
|
||||
$commands = $res['commands'];
|
||||
$scanPaths = $res['scan']['paths'];
|
||||
$publish = $res['publish'];
|
||||
$listeners = $res['listeners'];
|
||||
|
||||
$this->assertFalse(Arr::isAssoc($commands));
|
||||
$this->assertFalse(Arr::isAssoc($scanPaths));
|
||||
$this->assertFalse(Arr::isAssoc($listeners));
|
||||
$this->assertFalse(Arr::isAssoc($publish));
|
||||
$this->assertTrue(Arr::isAssoc($dependencies));
|
||||
}
|
||||
|
||||
public function testProviderConfigLoadProvidersHasCallable()
|
||||
{
|
||||
$res = ProviderConfig::loadProviders([
|
||||
FooConfigProvider::class,
|
||||
]);
|
||||
|
||||
foreach ($res['dependencies'] as $dependency) {
|
||||
$this->assertTrue(is_string($dependency) || is_callable($dependency));
|
||||
}
|
||||
}
|
||||
}
|
28
src/config/tests/Stub/Foo.php
Normal file
28
src/config/tests/Stub/Foo.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace HyperfTest\Config\Stub;
|
||||
|
||||
class Foo
|
||||
{
|
||||
public $id;
|
||||
|
||||
public function __construct($id = 0)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public static function make()
|
||||
{
|
||||
return new self(2);
|
||||
}
|
||||
}
|
29
src/config/tests/Stub/FooConfigProvider.php
Normal file
29
src/config/tests/Stub/FooConfigProvider.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace HyperfTest\Config\Stub;
|
||||
|
||||
class FooConfigProvider
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
return [
|
||||
'dependencies' => [
|
||||
'Foo' => function () {
|
||||
return new Foo(1);
|
||||
},
|
||||
'Foo2' => [Foo::class, 'make'],
|
||||
'Foo3' => Foo::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
26
src/config/tests/Stub/ProviderConfig.php
Normal file
26
src/config/tests/Stub/ProviderConfig.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace HyperfTest\Config\Stub;
|
||||
|
||||
class ProviderConfig extends \Hyperf\Config\ProviderConfig
|
||||
{
|
||||
public static function loadProviders(array $providers): array
|
||||
{
|
||||
return parent::loadProviders($providers);
|
||||
}
|
||||
|
||||
public static function merge(...$arrays): array
|
||||
{
|
||||
return parent::merge(...$arrays);
|
||||
}
|
||||
}
|
@ -49,32 +49,7 @@ class InitProxyCommand extends Command
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$scanDirs = $this->getScanDir();
|
||||
|
||||
$runtime = BASE_PATH . '/runtime/container/proxy/';
|
||||
if (is_dir($runtime)) {
|
||||
$this->clearRuntime($runtime);
|
||||
}
|
||||
|
||||
$classCollection = $this->scanner->scan($scanDirs);
|
||||
|
||||
foreach ($classCollection as $item) {
|
||||
try {
|
||||
$this->container->get($item);
|
||||
} catch (\Throwable $ex) {
|
||||
// Entry cannot be resoleved.
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->container instanceof Container) {
|
||||
foreach ($this->container->getDefinitionSource()->getDefinitions() as $key => $definition) {
|
||||
try {
|
||||
$this->container->get($key);
|
||||
} catch (\Throwable $ex) {
|
||||
// Entry cannot be resoleved.
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->createAopProxies();
|
||||
|
||||
$this->output->writeln('<info>Proxy class create success.</info>');
|
||||
}
|
||||
@ -111,4 +86,34 @@ class InitProxyCommand extends Command
|
||||
|
||||
return $scanDirs;
|
||||
}
|
||||
|
||||
private function createAopProxies()
|
||||
{
|
||||
$scanDirs = $this->getScanDir();
|
||||
|
||||
$runtime = BASE_PATH . '/runtime/container/proxy/';
|
||||
if (is_dir($runtime)) {
|
||||
$this->clearRuntime($runtime);
|
||||
}
|
||||
|
||||
$classCollection = $this->scanner->scan($scanDirs);
|
||||
|
||||
foreach ($classCollection as $item) {
|
||||
try {
|
||||
$this->container->get($item);
|
||||
} catch (\Throwable $ex) {
|
||||
// Entry cannot be resoleved.
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->container instanceof Container) {
|
||||
foreach ($this->container->getDefinitionSource()->getDefinitions() as $key => $definition) {
|
||||
try {
|
||||
$this->container->get($key);
|
||||
} catch (\Throwable $ex) {
|
||||
// Entry cannot be resoleved.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,9 +78,9 @@ class JsonRpcHttpTransporter implements TransporterInterface
|
||||
]);
|
||||
if ($response->getStatusCode() === 200) {
|
||||
return $response->getBody()->getContents();
|
||||
} else {
|
||||
$this->loadBalancer->removeNode($node);
|
||||
}
|
||||
$this->loadBalancer->removeNode($node);
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
|
149
src/json-rpc/tests/RpcServiceClientTest.php
Normal file
149
src/json-rpc/tests/RpcServiceClientTest.php
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace HyperfTest\JsonRpc;
|
||||
|
||||
use Hyperf\Config\Config;
|
||||
use Hyperf\Contract\ConfigInterface;
|
||||
use Hyperf\Contract\NormalizerInterface;
|
||||
use Hyperf\Contract\StdoutLoggerInterface;
|
||||
use Hyperf\Di\Annotation\Scanner;
|
||||
use Hyperf\Di\Container;
|
||||
use Hyperf\Di\Definition\DefinitionSource;
|
||||
use Hyperf\Di\MethodDefinitionCollector;
|
||||
use Hyperf\Di\MethodDefinitionCollectorInterface;
|
||||
use Hyperf\JsonRpc\DataFormatter;
|
||||
use Hyperf\JsonRpc\JsonRpcTransporter;
|
||||
use Hyperf\JsonRpc\NormalizeDataFormatter;
|
||||
use Hyperf\JsonRpc\PathGenerator;
|
||||
use Hyperf\Logger\Logger;
|
||||
use Hyperf\RpcClient\ProxyFactory;
|
||||
use Hyperf\Utils\ApplicationContext;
|
||||
use Hyperf\Utils\Packer\JsonPacker;
|
||||
use Hyperf\Utils\Serializer\SerializerFactory;
|
||||
use Hyperf\Utils\Serializer\SymfonyNormalizer;
|
||||
use HyperfTest\JsonRpc\Stub\CalculatorProxyServiceClient;
|
||||
use HyperfTest\JsonRpc\Stub\CalculatorServiceInterface;
|
||||
use HyperfTest\JsonRpc\Stub\IntegerValue;
|
||||
use Mockery\MockInterface;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class RpcServiceClientTest extends TestCase
|
||||
{
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testServiceClient()
|
||||
{
|
||||
$container = $this->createContainer();
|
||||
|
||||
/** @var MockInterface $transporter */
|
||||
$transporter = $container->get(JsonRpcTransporter::class);
|
||||
$transporter->shouldReceive('setLoadBalancer')
|
||||
->andReturnSelf();
|
||||
$transporter->shouldReceive('send')
|
||||
->andReturn(json_encode([
|
||||
'result' => 3,
|
||||
]));
|
||||
$service = new CalculatorProxyServiceClient($container, CalculatorServiceInterface::class, 'jsonrpc');
|
||||
$ret = $service->add(1, 2);
|
||||
$this->assertEquals(3, $ret);
|
||||
}
|
||||
|
||||
public function testProxyFactory()
|
||||
{
|
||||
$container = $this->createContainer();
|
||||
/** @var MockInterface $transporter */
|
||||
$transporter = $container->get(JsonRpcTransporter::class);
|
||||
$transporter->shouldReceive('setLoadBalancer')
|
||||
->andReturnSelf();
|
||||
$transporter->shouldReceive('send')
|
||||
->andReturn(json_encode([
|
||||
'result' => 3,
|
||||
]));
|
||||
$factory = new ProxyFactory();
|
||||
$proxyClass = $factory->createProxy(CalculatorServiceInterface::class);
|
||||
/** @var CalculatorServiceInterface $service */
|
||||
$service = new $proxyClass($container, CalculatorServiceInterface::class, 'jsonrpc');
|
||||
$ret = $service->add(1, 2);
|
||||
$this->assertEquals(3, $ret);
|
||||
}
|
||||
|
||||
public function testProxyFactoryObjectParameter()
|
||||
{
|
||||
$container = $this->createContainer();
|
||||
/** @var MockInterface $transporter */
|
||||
$transporter = $container->get(JsonRpcTransporter::class);
|
||||
$transporter->shouldReceive('setLoadBalancer')
|
||||
->andReturnSelf();
|
||||
$transporter->shouldReceive('send')
|
||||
->andReturn(json_encode([
|
||||
'result' => ['value' => 3],
|
||||
]));
|
||||
$factory = new ProxyFactory();
|
||||
$proxyClass = $factory->createProxy(CalculatorServiceInterface::class);
|
||||
/** @var CalculatorServiceInterface $service */
|
||||
$service = new $proxyClass($container, CalculatorServiceInterface::class, 'jsonrpc');
|
||||
$ret = $service->sum(IntegerValue::newInstance(1), IntegerValue::newInstance(2));
|
||||
$this->assertInstanceOf(IntegerValue::class, $ret);
|
||||
$this->assertEquals(3, $ret->getValue());
|
||||
}
|
||||
|
||||
public function createContainer()
|
||||
{
|
||||
$transporter = \Mockery::mock(JsonRpcTransporter::class);
|
||||
$container = new Container(new DefinitionSource([
|
||||
NormalizerInterface::class => SymfonyNormalizer::class,
|
||||
Serializer::class => SerializerFactory::class,
|
||||
DataFormatter::class => NormalizeDataFormatter::class,
|
||||
MethodDefinitionCollectorInterface::class => MethodDefinitionCollector::class,
|
||||
StdoutLoggerInterface::class => function () {
|
||||
return new Logger('App', [new StreamHandler('php://stderr')]);
|
||||
},
|
||||
ConfigInterface::class => function () {
|
||||
return new Config([
|
||||
'services' => [
|
||||
'consumers' => [
|
||||
[
|
||||
'name' => CalculatorServiceInterface::class,
|
||||
'nodes' => [
|
||||
['host' => '0.0.0.0', 'port' => 1234],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'protocols' => [
|
||||
'jsonrpc' => [
|
||||
'packer' => JsonPacker::class,
|
||||
'transporter' => JsonRpcTransporter::class,
|
||||
'path-generator' => PathGenerator::class,
|
||||
'data-formatter' => DataFormatter::class,
|
||||
],
|
||||
],
|
||||
]);
|
||||
},
|
||||
JsonRpcTransporter::class => function () use ($transporter) {
|
||||
return $transporter;
|
||||
},
|
||||
], [], new Scanner()));
|
||||
ApplicationContext::setContainer($container);
|
||||
return $container;
|
||||
}
|
||||
}
|
33
src/json-rpc/tests/Stub/CalculatorProxyServiceClient.php
Normal file
33
src/json-rpc/tests/Stub/CalculatorProxyServiceClient.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace HyperfTest\JsonRpc\Stub;
|
||||
|
||||
use Hyperf\RpcClient\Proxy\AbstractProxyService;
|
||||
|
||||
class CalculatorProxyServiceClient extends AbstractProxyService implements CalculatorServiceInterface
|
||||
{
|
||||
public function add(int $a, int $b)
|
||||
{
|
||||
return $this->client->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
public function sum(IntegerValue $a, IntegerValue $b): IntegerValue
|
||||
{
|
||||
return $this->client->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
|
||||
public function divide($value, $divider)
|
||||
{
|
||||
return $this->client->__call(__FUNCTION__, func_get_args());
|
||||
}
|
||||
}
|
@ -17,14 +17,13 @@ use Hyperf\Consul\Health;
|
||||
use Hyperf\Consul\HealthInterface;
|
||||
use Hyperf\Contract\ConfigInterface;
|
||||
use Hyperf\Contract\IdGeneratorInterface;
|
||||
use Hyperf\Contract\PackerInterface;
|
||||
use Hyperf\Guzzle\ClientFactory;
|
||||
use Hyperf\LoadBalancer\LoadBalancerInterface;
|
||||
use Hyperf\LoadBalancer\LoadBalancerManager;
|
||||
use Hyperf\LoadBalancer\Node;
|
||||
use Hyperf\Rpc\Contract\DataFormatterInterface;
|
||||
use Hyperf\Rpc\Contract\PathGeneratorInterface;
|
||||
use Hyperf\Rpc\Contract\TransporterInterface;
|
||||
use Hyperf\Rpc\Protocol;
|
||||
use Hyperf\Rpc\ProtocolManager;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Container\ContainerInterface;
|
||||
@ -61,7 +60,7 @@ abstract class AbstractServiceClient
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* @var ContainerInterfaces
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
@ -71,9 +70,9 @@ abstract class AbstractServiceClient
|
||||
protected $loadBalancerManager;
|
||||
|
||||
/**
|
||||
* @var \Hyperf\Rpc\ProtocolManager
|
||||
* @var null|\Hyperf\Contract\IdGeneratorInterface
|
||||
*/
|
||||
protected $protocolManager;
|
||||
protected $idGenerator;
|
||||
|
||||
/**
|
||||
* @var PathGeneratorInterface
|
||||
@ -85,31 +84,21 @@ abstract class AbstractServiceClient
|
||||
*/
|
||||
protected $dataFormatter;
|
||||
|
||||
/**
|
||||
* @var \Hyperf\Contract\ConfigInterface
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var null|\Hyperf\Contract\IdGeneratorInterface
|
||||
*/
|
||||
protected $idGenerator;
|
||||
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->loadBalancerManager = $container->get(LoadBalancerManager::class);
|
||||
$this->protocolManager = $container->get(ProtocolManager::class);
|
||||
$this->pathGenerator = $this->createPathGenerator();
|
||||
$this->dataFormatter = $this->createDataFormatter();
|
||||
$protocol = new Protocol($container, $container->get(ProtocolManager::class), $this->protocol);
|
||||
$loadBalancer = $this->createLoadBalancer(...$this->createNodes());
|
||||
$transporter = $this->createTransporter()->setLoadBalancer($loadBalancer);
|
||||
$transporter = $protocol->getTransporter()->setLoadBalancer($loadBalancer);
|
||||
$this->client = make(Client::class)
|
||||
->setPacker($this->createPacker())
|
||||
->setPacker($protocol->getPacker())
|
||||
->setTransporter($transporter);
|
||||
if ($container->has(IdGeneratorInterface::class)) {
|
||||
$this->idGenerator = $container->get(IdGeneratorInterface::class);
|
||||
}
|
||||
$this->pathGenerator = $protocol->getPathGenerator();
|
||||
$this->dataFormatter = $protocol->getDataFormatter();
|
||||
}
|
||||
|
||||
protected function __request(string $method, array $params, ?string $id = null)
|
||||
@ -149,46 +138,6 @@ abstract class AbstractServiceClient
|
||||
return $loadBalancer;
|
||||
}
|
||||
|
||||
protected function createTransporter(): TransporterInterface
|
||||
{
|
||||
$transporter = $this->protocolManager->getTransporter($this->protocol);
|
||||
if (! class_exists($transporter)) {
|
||||
throw new InvalidArgumentException(sprintf('Transporter %s is not exists.', $transporter));
|
||||
}
|
||||
/* @var TransporterInterface $instance */
|
||||
return make($transporter);
|
||||
}
|
||||
|
||||
protected function createPacker(): PackerInterface
|
||||
{
|
||||
$packer = $this->protocolManager->getPacker($this->protocol);
|
||||
if (! class_exists($packer)) {
|
||||
throw new InvalidArgumentException(sprintf('Packer %s is not exists.', $packer));
|
||||
}
|
||||
/* @var PackerInterface $packer */
|
||||
return $this->container->get($packer);
|
||||
}
|
||||
|
||||
protected function createPathGenerator(): PathGeneratorInterface
|
||||
{
|
||||
$pathGenerator = $this->protocolManager->getPathGenerator($this->protocol);
|
||||
if (! class_exists($pathGenerator)) {
|
||||
throw new InvalidArgumentException(sprintf('Path Generator %s is not exists.', $pathGenerator));
|
||||
}
|
||||
/* @var PathGeneratorInterface $pathGenerator */
|
||||
return $this->container->get($pathGenerator);
|
||||
}
|
||||
|
||||
protected function createDataFormatter(): DataFormatterInterface
|
||||
{
|
||||
$dataFormatter = $this->protocolManager->getDataFormatter($this->protocol);
|
||||
if (! class_exists($dataFormatter)) {
|
||||
throw new InvalidArgumentException(sprintf('Data Formatter %s is not exists.', $dataFormatter));
|
||||
}
|
||||
/* @var DataFormatterInterface $dataFormatter */
|
||||
return $this->container->get($dataFormatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create nodes the first time.
|
||||
*
|
||||
|
@ -12,6 +12,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Hyperf\RpcClient;
|
||||
|
||||
use Hyperf\RpcClient\Listener\AddConsumerDefinitionListener;
|
||||
|
||||
class ConfigProvider
|
||||
{
|
||||
public function __invoke(): array
|
||||
@ -21,6 +23,9 @@ class ConfigProvider
|
||||
],
|
||||
'commands' => [
|
||||
],
|
||||
'listeners' => [
|
||||
AddConsumerDefinitionListener::class,
|
||||
],
|
||||
'scan' => [
|
||||
'paths' => [
|
||||
__DIR__,
|
||||
|
17
src/rpc-client/src/Exception/RequestException.php
Normal file
17
src/rpc-client/src/Exception/RequestException.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\RpcClient\Exception;
|
||||
|
||||
class RequestException extends \RuntimeException
|
||||
{
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\RpcClient\Listener;
|
||||
|
||||
use Hyperf\Contract\ConfigInterface;
|
||||
use Hyperf\Di\Container;
|
||||
use Hyperf\Event\Contract\ListenerInterface;
|
||||
use Hyperf\Framework\Event\BootApplication;
|
||||
use Hyperf\RpcClient\ProxyFactory;
|
||||
use Hyperf\Utils\Arr;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class AddConsumerDefinitionListener implements ListenerInterface
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
private $container;
|
||||
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function listen(): array
|
||||
{
|
||||
return [
|
||||
BootApplication::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatic create proxy service definitions from services.consumers.
|
||||
*
|
||||
* @param BootApplication $event
|
||||
*/
|
||||
public function process(object $event)
|
||||
{
|
||||
/** @var Container $container */
|
||||
$container = $this->container;
|
||||
if ($container instanceof Container) {
|
||||
$consumers = $container->get(ConfigInterface::class)->get('services.consumers', []);
|
||||
$serviceFactory = $container->get(ProxyFactory::class);
|
||||
$definitions = $container->getDefinitionSource();
|
||||
foreach ($consumers as $consumer) {
|
||||
if (empty($consumer['name'])) {
|
||||
continue;
|
||||
}
|
||||
$serviceClass = $consumer['service'] ?? $consumer['name'];
|
||||
if (! interface_exists($serviceClass)) {
|
||||
continue;
|
||||
}
|
||||
$definitions->addDefinition(
|
||||
$consumer['id'] ?? $consumer['name'],
|
||||
function (ContainerInterface $container) use ($serviceFactory, $consumer, $serviceClass) {
|
||||
$proxyClass = $serviceFactory->createProxy($serviceClass);
|
||||
|
||||
return new $proxyClass(
|
||||
$container,
|
||||
$consumer['name'],
|
||||
$consumer['protocol'] ?? 'jsonrpc-http',
|
||||
Arr::only($consumer, ['load_balancer'])
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,6 @@ namespace Hyperf\RpcClient\Pool;
|
||||
|
||||
use Hyperf\Di\Container;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Swoole\Coroutine\Channel;
|
||||
|
||||
class PoolFactory
|
||||
{
|
||||
@ -24,7 +23,7 @@ class PoolFactory
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* @var Channel[]
|
||||
* @var RpcClientPool[]
|
||||
*/
|
||||
protected $pools = [];
|
||||
|
||||
@ -33,7 +32,7 @@ class PoolFactory
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function getPool(string $name): RedisPool
|
||||
public function getPool(string $name): RpcClientPool
|
||||
{
|
||||
if (isset($this->pools[$name])) {
|
||||
return $this->pools[$name];
|
||||
|
34
src/rpc-client/src/Proxy/AbstractProxyService.php
Normal file
34
src/rpc-client/src/Proxy/AbstractProxyService.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\RpcClient\Proxy;
|
||||
|
||||
use Hyperf\RpcClient\ServiceClient;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
abstract class AbstractProxyService
|
||||
{
|
||||
/**
|
||||
* @var ServiceClient
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
public function __construct(ContainerInterface $container, string $serviceName, string $protocol, array $options = [])
|
||||
{
|
||||
$this->client = make(ServiceClient::class, [
|
||||
'container' => $container,
|
||||
'serviceName' => $serviceName,
|
||||
'protocol' => $protocol,
|
||||
'options' => $options,
|
||||
]);
|
||||
}
|
||||
}
|
66
src/rpc-client/src/Proxy/Ast.php
Normal file
66
src/rpc-client/src/Proxy/Ast.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\RpcClient\Proxy;
|
||||
|
||||
use Hyperf\Utils\Composer;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\ParserFactory;
|
||||
use PhpParser\PrettyPrinter\Standard;
|
||||
use PhpParser\PrettyPrinterAbstract;
|
||||
|
||||
class Ast
|
||||
{
|
||||
/**
|
||||
* @var \PhpParser\Parser
|
||||
*/
|
||||
private $astParser;
|
||||
|
||||
/**
|
||||
* @var PrettyPrinterAbstract
|
||||
*/
|
||||
private $printer;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$parserFactory = new ParserFactory();
|
||||
$this->astParser = $parserFactory->create(ParserFactory::ONLY_PHP7);
|
||||
$this->printer = new Standard();
|
||||
}
|
||||
|
||||
public function proxy(string $className, string $proxyClassName)
|
||||
{
|
||||
if (! interface_exists($className)) {
|
||||
throw new \InvalidArgumentException("'{$className}' should be an interface name");
|
||||
}
|
||||
if (strpos($proxyClassName, '\\') !== false) {
|
||||
$exploded = explode('\\', $proxyClassName);
|
||||
$proxyClassName = end($exploded);
|
||||
}
|
||||
|
||||
$code = $this->getCodeByClassName($className);
|
||||
$stmts = $this->astParser->parse($code);
|
||||
$traverser = new NodeTraverser();
|
||||
$traverser->addVisitor(new ProxyCallVisitor($proxyClassName));
|
||||
$modifiedStmts = $traverser->traverse($stmts);
|
||||
return $this->printer->prettyPrintFile($modifiedStmts);
|
||||
}
|
||||
|
||||
public function getCodeByClassName(string $className): string
|
||||
{
|
||||
$file = Composer::getLoader()->findFile($className);
|
||||
if (! $file) {
|
||||
return '';
|
||||
}
|
||||
return file_get_contents($file);
|
||||
}
|
||||
}
|
57
src/rpc-client/src/Proxy/ProxyCallVisitor.php
Normal file
57
src/rpc-client/src/Proxy/ProxyCallVisitor.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\RpcClient\Proxy;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\Interface_;
|
||||
use PhpParser\NodeVisitorAbstract;
|
||||
|
||||
class ProxyCallVisitor extends NodeVisitorAbstract
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $classname;
|
||||
|
||||
public function __construct(string $classname)
|
||||
{
|
||||
$this->classname = $classname;
|
||||
}
|
||||
|
||||
public function leaveNode(Node $node)
|
||||
{
|
||||
if ($node instanceof Interface_) {
|
||||
return new Node\Stmt\Class_($this->classname, [
|
||||
'stmts' => $node->stmts,
|
||||
'extends' => new Node\Name\FullyQualified(AbstractProxyService::class),
|
||||
'implements' => [
|
||||
$node->name,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($node instanceof Node\Stmt\ClassMethod) {
|
||||
$node->stmts = [
|
||||
new Node\Stmt\Return_(new Node\Expr\MethodCall(
|
||||
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), new Node\Identifier('client')),
|
||||
new Node\Identifier('__call'),
|
||||
[
|
||||
new Node\Scalar\MagicConst\Function_(),
|
||||
new Node\Expr\FuncCall(new Node\Name('func_get_args')),
|
||||
]
|
||||
)),
|
||||
];
|
||||
return $node;
|
||||
}
|
||||
return parent::leaveNode($node); // TODO: Change the autogenerated stub
|
||||
}
|
||||
}
|
60
src/rpc-client/src/ProxyFactory.php
Normal file
60
src/rpc-client/src/ProxyFactory.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\RpcClient;
|
||||
|
||||
use Hyperf\RpcClient\Proxy\Ast;
|
||||
use Hyperf\Utils\Coroutine\Locker;
|
||||
use Hyperf\Utils\Traits\Container;
|
||||
|
||||
class ProxyFactory
|
||||
{
|
||||
use Container;
|
||||
|
||||
/**
|
||||
* @var Ast
|
||||
*/
|
||||
private $ast;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ast = new Ast();
|
||||
}
|
||||
|
||||
public function createProxy($serviceClass): string
|
||||
{
|
||||
if (self::has($serviceClass)) {
|
||||
return (string) self::get($serviceClass);
|
||||
}
|
||||
$dir = BASE_PATH . '/runtime/container/proxy/';
|
||||
if (! file_exists($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
$proxyFileName = str_replace('\\', '_', $serviceClass);
|
||||
$proxyClassName = $serviceClass . '_' . md5($this->ast->getCodeByClassName($serviceClass));
|
||||
$path = $dir . $proxyFileName . '.proxy.php';
|
||||
|
||||
$key = md5($path);
|
||||
// If the proxy file does not exist, then try to acquire the coroutine lock.
|
||||
if (! file_exists($path) && Locker::lock($key)) {
|
||||
$targetPath = $path . '.' . uniqid();
|
||||
$code = $this->ast->proxy($serviceClass, $proxyClassName);
|
||||
file_put_contents($targetPath, $code);
|
||||
rename($targetPath, $path);
|
||||
Locker::unlock($key);
|
||||
}
|
||||
include_once $path;
|
||||
self::set($serviceClass, $proxyClassName);
|
||||
return $proxyClassName;
|
||||
}
|
||||
}
|
88
src/rpc-client/src/ServiceClient.php
Normal file
88
src/rpc-client/src/ServiceClient.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace Hyperf\RpcClient;
|
||||
|
||||
use Hyperf\Contract\IdGeneratorInterface;
|
||||
use Hyperf\Contract\NormalizerInterface;
|
||||
use Hyperf\Di\MethodDefinitionCollectorInterface;
|
||||
use Hyperf\RpcClient\Exception\RequestException;
|
||||
use Hyperf\Utils\Arr;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
class ServiceClient extends AbstractServiceClient
|
||||
{
|
||||
/**
|
||||
* @var MethodDefinitionCollectorInterface
|
||||
*/
|
||||
protected $methodDefinitionCollector;
|
||||
|
||||
/**
|
||||
* @var NormalizerInterface
|
||||
*/
|
||||
private $normalizer;
|
||||
|
||||
public function __construct(ContainerInterface $container, string $serviceName, string $protocol = 'jsonrpc-http', array $options = [])
|
||||
{
|
||||
$this->serviceName = $serviceName;
|
||||
$this->protocol = $protocol;
|
||||
$this->setOptions($options);
|
||||
parent::__construct($container);
|
||||
$this->normalizer = $container->get(NormalizerInterface::class);
|
||||
$this->methodDefinitionCollector = $container->get(MethodDefinitionCollectorInterface::class);
|
||||
}
|
||||
|
||||
protected function __request(string $method, array $params, ?string $id = null)
|
||||
{
|
||||
if ($this->idGenerator instanceof IdGeneratorInterface && ! $id) {
|
||||
$id = $this->idGenerator->generate();
|
||||
}
|
||||
$response = $this->client->send($this->__generateData($method, $params, $id));
|
||||
if (! is_array($response)) {
|
||||
throw new RequestException('Invalid response.');
|
||||
}
|
||||
|
||||
if (isset($response['result'])) {
|
||||
$type = $this->methodDefinitionCollector->getReturnType($this->serviceName, $method);
|
||||
return $this->normalizer->denormalize($response['result'], $type->getName());
|
||||
}
|
||||
|
||||
if ($code = $response['error']['code'] ?? null) {
|
||||
$error = $response['error'];
|
||||
// Denormalize exception.
|
||||
$class = Arr::get($error, 'data.class');
|
||||
$attributes = Arr::get($error, 'data.attributes', []);
|
||||
if (isset($class) && class_exists($class) && $e = $this->normalizer->denormalize($attributes, $class)) {
|
||||
if ($e instanceof \Exception) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// Throw RequestException when denormalize exception failed.
|
||||
throw new RequestException($error['message'] ?? '', $error['code']);
|
||||
}
|
||||
|
||||
throw new RequestException('Invalid response.');
|
||||
}
|
||||
|
||||
public function __call(string $method, array $params)
|
||||
{
|
||||
return $this->__request($method, $params);
|
||||
}
|
||||
|
||||
protected function setOptions(array $options): void
|
||||
{
|
||||
if (isset($options['load_balancer'])) {
|
||||
$this->loadBalancer = $options['load_balancer'];
|
||||
}
|
||||
}
|
||||
}
|
@ -62,7 +62,7 @@ class Protocol
|
||||
if (! $this->container->has($transporter)) {
|
||||
throw new \InvalidArgumentException("Transporter {$transporter} for {$this->name} does not exist");
|
||||
}
|
||||
return $this->container->get($transporter);
|
||||
return make($transporter);
|
||||
}
|
||||
|
||||
public function getPathGenerator(): PathGeneratorInterface
|
||||
|
41
src/utils/tests/Traits/ContainerTest.php
Normal file
41
src/utils/tests/Traits/ContainerTest.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://doc.hyperf.io
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
namespace HyperfTest\Utils\Traits;
|
||||
|
||||
use Hyperf\Utils\Traits\Container;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class ContainerTest extends TestCase
|
||||
{
|
||||
public function testGet()
|
||||
{
|
||||
Foo::set('foo', 1);
|
||||
$this->assertNull(Bar::get('foo'));
|
||||
Bar::set('foo', 2);
|
||||
$this->assertEquals(1, Foo::get('foo'));
|
||||
$this->assertEquals(2, Bar::get('foo'));
|
||||
}
|
||||
}
|
||||
|
||||
class Foo
|
||||
{
|
||||
use Container;
|
||||
}
|
||||
class Bar
|
||||
{
|
||||
use Container;
|
||||
}
|
Loading…
Reference in New Issue
Block a user