diff --git a/.travis.yml b/.travis.yml
index 3593c90..8b845f5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,13 +5,14 @@ cache:
- $HOME/.composer/cache
php:
+ - '7.3'
- '7.4'
before_script:
- flags="-o"
- composer install $flags
-script: php ./vendor/phpunit/phpunit/phpunit -c phpunit.xml.dist
+script: composer run-script phpunit
after_success:
- - bash <(curl -s https://codecov.io/bash)
\ No newline at end of file
+ - bash <(curl -s https://codecov.io/bash)
diff --git a/composer.json b/composer.json
index 8b513b9..1a05623 100644
--- a/composer.json
+++ b/composer.json
@@ -17,13 +17,12 @@
"psr-4": { "RetailCrm\\": "src/" }
},
"require": {
- "php": ">=7.4.0",
+ "php": ">=7.3.0",
"ext-curl": "*",
"ext-json": "*",
"psr/http-client": "^1.0",
"symfony/serializer": "^5.1",
"symfony/validator": "^5.1",
- "devanych/di-container": "^2.1",
"doctrine/annotations": "^1.10",
"doctrine/cache": "^1.10",
"symfony/property-access": "^5.1"
@@ -32,7 +31,15 @@
"phpunit/phpunit": "^9.3",
"phpmd/phpmd": "^2.9",
"squizlabs/php_codesniffer": "^3.5",
- "guzzlehttp/guzzle": "^7.1"
+ "guzzlehttp/guzzle": "^7.1",
+ "phpcompatibility/php-compatibility": "*",
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0"
},
+ "scripts": {
+ "phpunit": "./vendor/bin/phpunit -c phpunit.xml.dist",
+ "phpcs": "./vendor/bin/phpcs -p src --runtime-set testVersion 7.3",
+ "phpcbf": "./vendor/bin/phpcbf -p src"
+ },
+ "prefer-stable": true,
"license": "MIT"
}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index d9a8450..b56ded7 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,36 +1,36 @@
-
-
-
-
- tests
-
-
-
-
-
- src
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+ src
+
+
+
+
+
+
+
+ tests
+
+
+
+
+
+
diff --git a/src/Component/DependencyInjection/Container.php b/src/Component/DependencyInjection/Container.php
new file mode 100644
index 0000000..b791be5
--- /dev/null
+++ b/src/Component/DependencyInjection/Container.php
@@ -0,0 +1,280 @@
+
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+namespace RetailCrm\Component\DependencyInjection;
+
+use Closure;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
+use ReflectionClass;
+use ReflectionException;
+use RetailCrm\Component\DependencyInjection\Exception\ContainerException;
+use RetailCrm\Component\DependencyInjection\Exception\NotFoundException;
+use Throwable;
+
+/**
+ * Class Container
+ * This implementation took an inspiration from devanych/di-container. It's almost the same, but for PHP 7.3.
+ *
+ * @category Container
+ * @package RetailCrm\Component\DependencyInjection
+ * @author Evgeniy Zyubin
+ * @author RetailDriver LLC
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ */
+class Container implements ContainerInterface
+{
+ /**
+ * @var array $instances
+ */
+ public $instances = [];
+
+ /**
+ * @var array $definitions
+ */
+ public $definitions = [];
+
+ /**
+ * Container constructor.
+ *
+ * @param array $definitions
+ */
+ public function __construct(array $definitions = [])
+ {
+ $this->setGroup($definitions);
+ }
+
+ /**
+ * Sets definition to the container.
+ *
+ * @param string $id
+ * @param mixed $definition
+ */
+ public function set(string $id, $definition): void
+ {
+ if ($this->instantiated($id)) {
+ unset($this->instances[$id]);
+ }
+
+ $this->definitions[$id] = $definition;
+ }
+
+ /**
+ * Sets group of definitions at once.
+ *
+ * @param array $definitions
+ */
+ public function setGroup(array $definitions): void
+ {
+ foreach ($definitions as $id => $definition) {
+ self::isString($id);
+ $this->set($id, $definition);
+ }
+ }
+
+ /**
+ * Finds an entry of the container by its identifier and returns it.
+ *
+ * @param string $id Identifier of the entry to look for.
+ *
+ * @return mixed Entry.
+ * @throws ContainerExceptionInterface Error while retrieving the entry.
+ *
+ * @throws NotFoundExceptionInterface No entry was found for **this** identifier.
+ */
+ public function get($id)
+ {
+ self::isString($id);
+
+ if ($this->instantiated($id)) {
+ return $this->instances[$id];
+ }
+
+ $this->instances[$id] = $this->createInstance($id);
+
+ return $this->instances[$id];
+ }
+
+ /**
+ * Returns true if the container can return an entry for the given identifier.
+ * Returns false otherwise.
+ *
+ * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
+ * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
+ *
+ * @param string $id Identifier of the entry to look for.
+ *
+ * @return bool
+ */
+ public function has($id): bool
+ {
+ return (is_string($id) && array_key_exists($id, $this->definitions));
+ }
+
+ /**
+ * Create instance by definition from the container by ID.
+ *
+ * @param string $id
+ * @return mixed
+ * @throws NotFoundException
+ * @throws ContainerException
+ */
+ private function createInstance(string $id)
+ {
+ if (!$this->has($id)) {
+ if (self::isClassName($id)) {
+ return $this->createObject($id);
+ }
+
+ throw new NotFoundException(sprintf('`%s` is not set in container and is not a class name.', $id));
+ }
+
+ if (self::isClassName($this->definitions[$id])) {
+ return $this->createObject($this->definitions[$id]);
+ }
+
+ if ($this->definitions[$id] instanceof Closure) {
+ return $this->definitions[$id]($this);
+ }
+
+ return $this->definitions[$id];
+ }
+
+ /**
+ * Create object by class name.
+ *
+ * @param string $className
+ * @return object
+ * @throws ContainerException If unable to create object.
+ */
+ private function createObject(string $className): object
+ {
+ try {
+ $reflection = new ReflectionClass($className);
+ } catch (ReflectionException $e) {
+ throw new ContainerException(sprintf('Unable to create object `%s`.', $className), 0, $e);
+ }
+
+ if (in_array(FactoryInterface::class, $reflection->getInterfaceNames(), true)) {
+ try {
+ /*** @var FactoryInterface $factory */
+ $factory = $this->getObjectFromReflection($reflection);
+ return $factory->create($this);
+ } catch (ContainerException $e) {
+ throw $e;
+ } catch (Throwable $e) {
+ throw new ContainerException(sprintf('Unable to create object `%s`.', $className), 0, $e);
+ }
+ }
+
+ return $this->getObjectFromReflection($reflection);
+ }
+
+ /**
+ * Create object from reflection.
+ *
+ * If the object has dependencies in the constructor, it tries to create them too.
+ *
+ * @param ReflectionClass $reflection
+ * @return object
+ * @throws ContainerException If unable to create object.
+ */
+ private function getObjectFromReflection(ReflectionClass $reflection): object
+ {
+ if (($constructor = $reflection->getConstructor()) === null) {
+ return $reflection->newInstance();
+ }
+
+ $arguments = [];
+
+ foreach ($constructor->getParameters() as $parameter) {
+ if ($type = $parameter->getType()) {
+ $typeName = $type->getName();
+
+ if (!$type->isBuiltin() && ($this->has($typeName) || self::isClassName($typeName))) {
+ $arguments[] = $this->get($typeName);
+ continue;
+ }
+
+ if ($typeName === 'array' && $type->isBuiltin() && !$parameter->isDefaultValueAvailable()) {
+ $arguments[] = [];
+ continue;
+ }
+ }
+
+ if ($parameter->isDefaultValueAvailable()) {
+ try {
+ $arguments[] = $parameter->getDefaultValue();
+ continue;
+ } catch (ReflectionException $e) {
+ throw new ContainerException(
+ sprintf(
+ 'Unable to create object `%s`. Unable to get default value of constructor parameter: `%s`.',
+ $reflection->getName(),
+ $parameter->getName()
+ )
+ );
+ }
+ }
+
+ throw new ContainerException(
+ sprintf(
+ 'Unable to create object `%s`. Unable to process a constructor parameter: `%s`.',
+ $reflection->getName(),
+ $parameter->getName()
+ )
+ );
+ }
+
+ return $reflection->newInstanceArgs($arguments);
+ }
+
+ /**
+ * Returns true if definition was instantiated, false otherwise.
+ *
+ * @param string $id
+ *
+ * @return bool
+ */
+ private function instantiated(string $id): bool
+ {
+ return array_key_exists($id, $this->instances);
+ }
+
+ /**
+ * Returns true if provided parameter is class name, false otherwise.
+ *
+ * @param mixed $className
+ *
+ * @return bool
+ */
+ private static function isClassName($className): bool
+ {
+ return (is_string($className) && class_exists($className));
+ }
+
+ /**
+ * Validate provided ID
+ *
+ * @param mixed $id
+ * @throws NotFoundException
+ */
+ private static function isString($id): void
+ {
+ if (!is_string($id)) {
+ throw new NotFoundException(sprintf('Invalid id. Expects string, `%s` provided.', gettype($id)));
+ }
+ }
+}
diff --git a/src/Component/DependencyInjection/Exception/ContainerException.php b/src/Component/DependencyInjection/Exception/ContainerException.php
new file mode 100644
index 0000000..beed645
--- /dev/null
+++ b/src/Component/DependencyInjection/Exception/ContainerException.php
@@ -0,0 +1,31 @@
+
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+namespace RetailCrm\Component\DependencyInjection\Exception;
+
+use Psr\Container\ContainerExceptionInterface;
+use LogicException;
+
+/**
+ * Class ContainerException
+ *
+ * @category ContainerException
+ * @package RetailCrm\Component\DependencyInjection\Exception
+ * @author Evgeniy Zyubin
+ * @author RetailDriver LLC
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ */
+class ContainerException extends LogicException implements ContainerExceptionInterface
+{
+}
diff --git a/src/Component/DependencyInjection/Exception/NotFoundException.php b/src/Component/DependencyInjection/Exception/NotFoundException.php
new file mode 100644
index 0000000..9207e2d
--- /dev/null
+++ b/src/Component/DependencyInjection/Exception/NotFoundException.php
@@ -0,0 +1,31 @@
+
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+namespace RetailCrm\Component\DependencyInjection\Exception;
+
+use Psr\Container\NotFoundExceptionInterface;
+use InvalidArgumentException;
+
+/**
+ * Class NotFoundException
+ *
+ * @category NotFoundException
+ * @package RetailCrm\Component\DependencyInjection\Exception
+ * @author Evgeniy Zyubin
+ * @author RetailDriver LLC
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ */
+class NotFoundException extends InvalidArgumentException implements NotFoundExceptionInterface
+{
+}
diff --git a/src/Component/DependencyInjection/FactoryInterface.php b/src/Component/DependencyInjection/FactoryInterface.php
new file mode 100644
index 0000000..a79e492
--- /dev/null
+++ b/src/Component/DependencyInjection/FactoryInterface.php
@@ -0,0 +1,36 @@
+
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+namespace RetailCrm\Component\DependencyInjection;
+
+use Psr\Container\ContainerInterface;
+
+/**
+ * Interface FactoryInterface
+ *
+ * @category FactoryInterface
+ * @package RetailCrm\Component\DependencyInjection
+ * @author Evgeniy Zyubin
+ * @author RetailDriver LLC
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ */
+interface FactoryInterface
+{
+ /**
+ * @param \Psr\Container\ContainerInterface $container
+ *
+ * @return object
+ */
+ public function create(ContainerInterface $container): object;
+}
diff --git a/src/Component/Environment.php b/src/Component/Environment.php
index 4a64920..ec2a58d 100644
--- a/src/Component/Environment.php
+++ b/src/Component/Environment.php
@@ -1,7 +1,7 @@
container = $container;
- }
-
- /**
- * @return \Psr\Container\ContainerInterface
- */
- public function getContainer(): ContainerInterface
- {
- return $this->container;
- }
-
- /**
- * @param string $serviceUrl
- *
- * @return \RetailCrm\TopClient\Client
- * @throws \RetailCrm\Component\Exception\ValidationException
- */
- public function createClient(string $serviceUrl): Client
- {
- $factory = $this->container->get(ClientFactory::class);
-
- if (!($factory instanceof ClientFactory)) {
- throw new \RuntimeException('Invalid factory definition in the provided container');
- }
-
- return $factory->create($serviceUrl);
- }
}
diff --git a/src/Component/Exception/ValidationException.php b/src/Component/Exception/ValidationException.php
index e9accde..6411f10 100644
--- a/src/Component/Exception/ValidationException.php
+++ b/src/Component/Exception/ValidationException.php
@@ -1,7 +1,7 @@
setContainer($container);
+
+ return $factory;
+ }
/**
* @param string $serviceUrl
*
+ * @return $this
+ */
+ public function setServiceUrl(string $serviceUrl): ClientFactory
+ {
+ $this->serviceUrl = $serviceUrl;
+ return $this;
+ }
+
+ /**
* @return \RetailCrm\TopClient\Client
* @throws \RetailCrm\Component\Exception\ValidationException
*/
- public function create(string $serviceUrl): Client
+ public function create(): Client
{
- $client = new Client($serviceUrl);
- $client->setHttpClient($this->httpClient);
- $client->setSerializer($this->serializer);
- $client->setValidator($this->validator);
+ $client = new Client($this->serviceUrl);
+ $client->setHttpClient($this->container->get('httpClient'));
+ $client->setSerializer($this->container->get('serializer'));
+ $client->setValidator($this->container->get('validator'));
$client->validateSelf();
return $client;
diff --git a/src/Factory/EnvironmentFactory.php b/src/Factory/ContainerFactory.php
similarity index 57%
rename from src/Factory/EnvironmentFactory.php
rename to src/Factory/ContainerFactory.php
index 05a1d16..121d419 100644
--- a/src/Factory/EnvironmentFactory.php
+++ b/src/Factory/ContainerFactory.php
@@ -1,7 +1,7 @@
env = $environmentType;
@@ -57,28 +63,35 @@ class EnvironmentFactory implements EnvironmentAwareFactoryInterface
/**
* @param \Psr\Http\Client\ClientInterface $httpClient
*
- * @return \RetailCrm\Component\Environment
+ * @return \RetailCrm\Factory\ContainerFactory
*/
- public function create(ClientInterface $httpClient): Environment
+ public function withClient(ClientInterface $httpClient): ContainerFactory
{
$this->httpClient = $httpClient;
+ return $this;
+ }
+ /**
+ * @return \Psr\Container\ContainerInterface
+ */
+ public function create(): ContainerInterface
+ {
$container = new Container();
switch ($this->env) {
- case Environment::PROD:
- $this->setProdServices($container);
- break;
- case Environment::DEV:
- case Environment::TEST:
- $this->setProdServices($container);
- $this->setDevServices($container);
- break;
- default:
- throw new \RuntimeException(sprintf('Invalid environment type: %s', $this->env));
+ case Environment::PROD:
+ $this->setProdServices($container);
+ break;
+ case Environment::DEV:
+ case Environment::TEST:
+ $this->setProdServices($container);
+ $this->setDevServices($container);
+ break;
+ default:
+ throw new \RuntimeException(sprintf('Invalid environment type: %s', $this->env));
}
- return new Environment($container);
+ return $container;
}
/**
@@ -88,18 +101,12 @@ class EnvironmentFactory implements EnvironmentAwareFactoryInterface
{
$container->set('httpClient', $this->httpClient);
$container->set('validator', Validation::createValidatorBuilder()->enableAnnotationMapping()->getValidator());
- $container->set('serializer', new Serializer(
- [new ObjectNormalizer()],
- [new XmlEncoder(), new JsonEncoder()]
- ));
- $container->set(ClientFactory::class, function (ContainerInterface $container): ClientFactory {
- $factory = new ClientFactory();
- $factory->setHttpClient($container->get('httpClient'));
- $factory->setSerializer($container->get('serializer'));
- $factory->setValidator($container->get('validator'));
-
- return $factory;
- });
+ $container->set(
+ 'serializer', new Serializer(
+ [new ObjectNormalizer()],
+ [new XmlEncoder(), new JsonEncoder()]
+ )
+ );
}
/**
diff --git a/src/Factory/EnvironmentAwareFactoryInterface.php b/src/Factory/EnvironmentAwareFactoryInterface.php
deleted file mode 100644
index e03ca96..0000000
--- a/src/Factory/EnvironmentAwareFactoryInterface.php
+++ /dev/null
@@ -1,42 +0,0 @@
-
- * @license MIT
- * @link http://retailcrm.ru
- * @see http://help.retailcrm.ru
- */
-
-namespace RetailCrm\Factory;
-
-use Psr\Http\Client\ClientInterface;
-use RetailCrm\Component\Environment;
-
-/**
- * Interface EnvironmentAwareFactoryInterface
- *
- * @category EnvironmentAwareFactoryInterface
- * @package RetailCrm\Factory
- * @author RetailDriver LLC
- * @license MIT
- * @link http://retailcrm.ru
- * @see https://help.retailcrm.ru
- */
-interface EnvironmentAwareFactoryInterface
-{
- /**
- * @param string $environmentType
- *
- * @return \RetailCrm\Factory\EnvironmentAwareFactoryInterface
- */
- public static function withEnv(string $environmentType = Environment::DEV): EnvironmentAwareFactoryInterface;
-
- /**
- * @return \RetailCrm\Component\Environment
- */
- public function create(ClientInterface $httpClient): Environment;
-}
diff --git a/src/Interfaces/ContainerAwareInterface.php b/src/Interfaces/ContainerAwareInterface.php
new file mode 100644
index 0000000..ef62e6a
--- /dev/null
+++ b/src/Interfaces/ContainerAwareInterface.php
@@ -0,0 +1,41 @@
+
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+
+namespace RetailCrm\Interfaces;
+
+use Psr\Container\ContainerInterface;
+
+/**
+ * Interface ContainerAwareInterface
+ *
+ * @category ContainerAwareInterface
+ * @package RetailCrm\Interfaces
+ * @author RetailDriver LLC
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ */
+interface ContainerAwareInterface
+{
+ /**
+ * @param \Psr\Container\ContainerInterface $container
+ *
+ * @return mixed
+ */
+ public function setContainer(ContainerInterface $container): void;
+
+ /**
+ * @return \Psr\Container\ContainerInterface
+ */
+ public function getContainer(): ContainerInterface;
+}
diff --git a/src/Interfaces/FactoryInterface.php b/src/Interfaces/FactoryInterface.php
new file mode 100644
index 0000000..baa11e9
--- /dev/null
+++ b/src/Interfaces/FactoryInterface.php
@@ -0,0 +1,32 @@
+
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+
+namespace RetailCrm\Interfaces;
+
+/**
+ * Interface FactoryInterface
+ *
+ * @category FactoryInterface
+ * @package RetailCrm\Interfaces
+ * @author RetailDriver LLC
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ */
+interface FactoryInterface
+{
+ /**
+ * @return object
+ */
+ public function create();
+}
diff --git a/src/Interfaces/HttpClientAwareInterface.php b/src/Interfaces/HttpClientAwareInterface.php
index e7ae373..c39dd59 100644
--- a/src/Interfaces/HttpClientAwareInterface.php
+++ b/src/Interfaces/HttpClientAwareInterface.php
@@ -1,7 +1,7 @@
validator->validate($this);
-
- if ($violations->count()) {
- throw new ValidationException("Invalid client data", $violations);
- }
+ $this->validate($this);
}
/**
@@ -79,7 +76,7 @@ class Client implements SerializerAwareInterface, HttpClientAwareInterface, Vali
$violations = $this->validator->validate($item);
if ($violations->count()) {
- throw new ValidationException("Invalid data", $item);
+ throw new ValidationException("Invalid data", $violations);
}
}
}
diff --git a/src/Traits/ContainerAwareTrait.php b/src/Traits/ContainerAwareTrait.php
new file mode 100644
index 0000000..6d4fa28
--- /dev/null
+++ b/src/Traits/ContainerAwareTrait.php
@@ -0,0 +1,52 @@
+
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+
+namespace RetailCrm\Traits;
+
+use Psr\Container\ContainerInterface;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * Trait ContainerAwareTrait
+ *
+ * @category ContainerAwareTrait
+ * @package RetailCrm\Traits
+ * @author RetailDriver LLC
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ */
+trait ContainerAwareTrait
+{
+ /**
+ * @var ContainerInterface $container
+ * @Assert\NotNull(message="Container should be provided")
+ */
+ protected $container;
+
+ /**
+ * @param \Psr\Container\ContainerInterface $container
+ */
+ public function setContainer(ContainerInterface $container): void
+ {
+ $this->container = $container;
+ }
+
+ /**
+ * @return \Psr\Container\ContainerInterface
+ */
+ public function getContainer(): ContainerInterface
+ {
+ return $this->container;
+ }
+}
diff --git a/src/Traits/HttpClientAwareTrait.php b/src/Traits/HttpClientAwareTrait.php
index 5cc450b..db9fbe6 100644
--- a/src/Traits/HttpClientAwareTrait.php
+++ b/src/Traits/HttpClientAwareTrait.php
@@ -1,7 +1,7 @@
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+
+namespace RetailCrm\Traits;
+
+use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * Trait SerializerAwareTrait
+ *
+ * @category SerializerAwareTrait
+ * @package RetailCrm\Traits
+ * @author Joel Wurtz
+ * @author RetailDriver LLC
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ */
+trait SerializerAwareTrait
+{
+ /**
+ * @var SerializerInterface $serializer
+ * @Assert\NotNull(message="Serializer should be provided")
+ */
+ protected $serializer;
+
+ /**
+ * @param \Symfony\Component\Serializer\SerializerInterface $serializer
+ */
+ public function setSerializer(SerializerInterface $serializer): void
+ {
+ $this->serializer = $serializer;
+ }
+}
diff --git a/src/Traits/ValidatorAwareTrait.php b/src/Traits/ValidatorAwareTrait.php
index 8abe8b4..8902972 100644
--- a/src/Traits/ValidatorAwareTrait.php
+++ b/src/Traits/ValidatorAwareTrait.php
@@ -1,7 +1,7 @@
create(new HttpClient())
- ->createClient(Client::OVERSEAS_ENDPOINT);
+ $client = ClientFactory::withContainer(
+ ContainerFactory::withEnv(Environment::DEV)
+ ->withClient(new \GuzzleHttp\Client())
+ ->create()
+ )->setServiceUrl(Client::OVERSEAS_ENDPOINT)->create();
self::assertInstanceOf(Client::class, $client);
}