diff --git a/composer.json b/composer.json
index 19bc629..cc0c954 100644
--- a/composer.json
+++ b/composer.json
@@ -20,23 +20,31 @@
"php": ">=7.3.0",
"ext-curl": "*",
"ext-json": "*",
- "psr/http-client": "^1.0",
- "symfony/validator": "^5.1",
- "jms/serializer": "^3.9",
- "shieldon/psr-http": "^1.2",
- "doctrine/annotations": "^1.10",
+ "psr/log": "^1.1",
"doctrine/cache": "^1.10",
- "psr/log": "^1.1"
+ "jms/serializer": "^3.9",
+ "symfony/validator": "^5.1",
+ "doctrine/annotations": "^1.10",
+ "psr/http-client": "^1.0",
+ "psr/http-message": "^1.0",
+ "php-http/client-implementation": "^1.0",
+ "php-http/httplug": "^2.2",
+ "php-http/message-factory": "^1.0",
+ "php-http/discovery": "^1.12",
+ "php-http/multipart-stream-builder": "^1.1"
},
"require-dev": {
"phpunit/phpunit": "^9.3",
"phpmd/phpmd": "^2.9",
"squizlabs/php_codesniffer": "^3.5",
- "guzzlehttp/guzzle": "^7.1",
"phpcompatibility/php-compatibility": "*",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
"vlucas/phpdotenv": "^5.2",
- "brainmaestro/composer-git-hooks": "^2.8"
+ "brainmaestro/composer-git-hooks": "^2.8",
+ "php-http/mock-client": "^1.4",
+ "php-http/message": "^1.9",
+ "php-http/curl-client": "^2.1",
+ "nyholm/psr7": "^1.3"
},
"scripts": {
"cghooks": "vendor/bin/cghooks",
diff --git a/phpmd.xml b/phpmd.xml
index 65f6fc3..7795826 100644
--- a/phpmd.xml
+++ b/phpmd.xml
@@ -29,7 +29,7 @@
-
+
diff --git a/src/Builder/ClientBuilder.php b/src/Builder/ClientBuilder.php
index cdc1b09..84b858c 100644
--- a/src/Builder/ClientBuilder.php
+++ b/src/Builder/ClientBuilder.php
@@ -14,13 +14,14 @@ namespace RetailCrm\Builder;
use RetailCrm\Component\Constants;
use RetailCrm\Component\ServiceLocator;
-use RetailCrm\Factory\RequestFactory;
+use RetailCrm\Factory\TopRequestFactory;
use RetailCrm\Interfaces\AppDataInterface;
use RetailCrm\Interfaces\AuthenticatorInterface;
use RetailCrm\Interfaces\BuilderInterface;
use RetailCrm\Interfaces\ContainerAwareInterface;
-use RetailCrm\Interfaces\RequestFactoryInterface;
+use RetailCrm\Interfaces\TopRequestFactoryInterface;
use RetailCrm\Interfaces\RequestTimestampProviderInterface;
+use RetailCrm\Interfaces\TopRequestProcessorInterface;
use RetailCrm\TopClient\Client;
use RetailCrm\Traits\ContainerAwareTrait;
@@ -84,8 +85,9 @@ class ClientBuilder implements ContainerAwareInterface, BuilderInterface
$client->setHttpClient($this->container->get(Constants::HTTP_CLIENT));
$client->setSerializer($this->container->get(Constants::SERIALIZER));
$client->setValidator($this->container->get(Constants::VALIDATOR));
- $client->setRequestFactory($this->container->get(RequestFactoryInterface::class));
+ $client->setRequestFactory($this->container->get(TopRequestFactoryInterface::class));
$client->setServiceLocator($this->container->get(ServiceLocator::class));
+ $client->setProcessor($this->container->get(TopRequestProcessorInterface::class));
$client->validateSelf();
return $client;
diff --git a/src/Builder/ContainerBuilder.php b/src/Builder/ContainerBuilder.php
index 5f5ff80..c73bf1a 100644
--- a/src/Builder/ContainerBuilder.php
+++ b/src/Builder/ContainerBuilder.php
@@ -12,8 +12,13 @@
*/
namespace RetailCrm\Builder;
+use Http\Discovery\Psr17FactoryDiscovery;
+use Http\Discovery\Psr18ClientDiscovery;
use Psr\Container\ContainerInterface;
use Psr\Http\Client\ClientInterface;
+use Psr\Http\Message\RequestFactoryInterface;
+use Psr\Http\Message\StreamFactoryInterface;
+use Psr\Http\Message\UriFactoryInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use RetailCrm\Component\Constants;
@@ -21,18 +26,19 @@ use RetailCrm\Component\DependencyInjection\Container;
use RetailCrm\Component\Environment;
use RetailCrm\Component\ServiceLocator;
use RetailCrm\Factory\FileItemFactory;
-use RetailCrm\Factory\RequestFactory;
use RetailCrm\Factory\SerializerFactory;
+use RetailCrm\Factory\TopRequestFactory;
use RetailCrm\Interfaces\BuilderInterface;
use RetailCrm\Interfaces\FileItemFactoryInterface;
-use RetailCrm\Interfaces\RequestFactoryInterface;
use RetailCrm\Interfaces\RequestSignerInterface;
use RetailCrm\Interfaces\RequestTimestampProviderInterface;
+use RetailCrm\Interfaces\TopRequestFactoryInterface;
+use RetailCrm\Interfaces\TopRequestProcessorInterface;
use RetailCrm\Service\RequestDataFilter;
use RetailCrm\Service\RequestSigner;
use RetailCrm\Service\RequestTimestampProvider;
+use RetailCrm\Service\TopRequestProcessor;
use RuntimeException;
-use Shieldon\Psr17\StreamFactory;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Validator\TraceableValidator;
use Symfony\Component\Validator\Validator\ValidatorInterface;
@@ -55,7 +61,7 @@ class ContainerBuilder implements BuilderInterface
/**
* @var string $env
*/
- private $env;
+ private $env = Environment::DEV;
/**
* @var \Psr\Http\Client\ClientInterface $httpClient
@@ -67,6 +73,21 @@ class ContainerBuilder implements BuilderInterface
*/
private $logger;
+ /**
+ * @var StreamFactoryInterface $streamFactory
+ */
+ private $streamFactory;
+
+ /**
+ * @var RequestFactoryInterface $requestFactory
+ */
+ private $requestFactory;
+
+ /**
+ * @var UriFactoryInterface $uriFactory
+ */
+ private $uriFactory;
+
/**
* @return static
*/
@@ -108,6 +129,39 @@ class ContainerBuilder implements BuilderInterface
return $this;
}
+ /**
+ * @param \Psr\Http\Message\StreamFactoryInterface $streamFactory
+ *
+ * @return ContainerBuilder
+ */
+ public function setStreamFactory(StreamFactoryInterface $streamFactory): ContainerBuilder
+ {
+ $this->streamFactory = $streamFactory;
+ return $this;
+ }
+
+ /**
+ * @param \Psr\Http\Message\RequestFactoryInterface $requestFactory
+ *
+ * @return ContainerBuilder
+ */
+ public function setRequestFactory(RequestFactoryInterface $requestFactory): ContainerBuilder
+ {
+ $this->requestFactory = $requestFactory;
+ return $this;
+ }
+
+ /**
+ * @param \Psr\Http\Message\UriFactoryInterface $uriFactory
+ *
+ * @return ContainerBuilder
+ */
+ public function setUriFactory(UriFactoryInterface $uriFactory): ContainerBuilder
+ {
+ $this->uriFactory = $uriFactory;
+ return $this;
+ }
+
/**
* @return \Psr\Container\ContainerInterface
*/
@@ -136,8 +190,11 @@ class ContainerBuilder implements BuilderInterface
*/
protected function setProdServices(Container $container): void
{
- $container->set(Constants::HTTP_CLIENT, $this->httpClient);
- $container->set(Constants::LOGGER, $this->logger ?: new NullLogger());
+ $container->set(Constants::HTTP_CLIENT, $this->getHttpClient());
+ $container->set(Constants::LOGGER, $this->getLogger());
+ $container->set(StreamFactoryInterface::class, $this->getStreamFactory());
+ $container->set(RequestFactoryInterface::class, $this->getRequestFactory());
+ $container->set(UriFactoryInterface::class, $this->getUriFactory());
$container->set(RequestTimestampProviderInterface::class, new RequestTimestampProvider());
$container->set(
Constants::VALIDATOR,
@@ -146,7 +203,9 @@ class ContainerBuilder implements BuilderInterface
$container->set(Constants::SERIALIZER, function (ContainerInterface $container) {
return SerializerFactory::withContainer($container)->create();
});
- $container->set(FileItemFactoryInterface::class, new FileItemFactory(new StreamFactory()));
+ $container->set(FileItemFactoryInterface::class, function (ContainerInterface $container) {
+ return new FileItemFactory($container->get(StreamFactoryInterface::class));
+ });
$container->set(RequestDataFilter::class, new RequestDataFilter());
$container->set(RequestSignerInterface::class, function (ContainerInterface $container) {
return new RequestSigner(
@@ -154,14 +213,19 @@ class ContainerBuilder implements BuilderInterface
$container->get(RequestDataFilter::class)
);
});
- $container->set(RequestFactoryInterface::class, function (ContainerInterface $container) {
- return new RequestFactory(
- $container->get(RequestSignerInterface::class),
- $container->get(RequestDataFilter::class),
- $container->get(Constants::SERIALIZER),
- $container->get(Constants::VALIDATOR),
- $container->get(RequestTimestampProviderInterface::class)
- );
+ $container->set(TopRequestProcessorInterface::class, function (ContainerInterface $container) {
+ return (new TopRequestProcessor())
+ ->setSigner($container->get(RequestSignerInterface::class))
+ ->setValidator($container->get(Constants::VALIDATOR))
+ ->setTimestampProvider($container->get(RequestTimestampProviderInterface::class));
+ });
+ $container->set(TopRequestFactoryInterface::class, function (ContainerInterface $container) {
+ return (new TopRequestFactory())
+ ->setFilter($container->get(RequestDataFilter::class))
+ ->setSerializer($container->get(Constants::SERIALIZER))
+ ->setStreamFactory($container->get(StreamFactoryInterface::class))
+ ->setRequestFactory($container->get(RequestFactoryInterface::class))
+ ->setUriFactory($container->get(UriFactoryInterface::class));
});
$container->set(ServiceLocator::class, function (ContainerInterface $container) {
$locator = new ServiceLocator();
@@ -182,4 +246,50 @@ class ContainerBuilder implements BuilderInterface
$container->set('validator', new TraceableValidator($validator));
}
}
+
+ /**
+ * @return \Psr\Http\Client\ClientInterface
+ */
+ protected function getHttpClient(): ClientInterface
+ {
+ return $this->httpClient instanceof ClientInterface ? $this->httpClient : Psr18ClientDiscovery::find();
+ }
+
+ /**
+ * @return \Psr\Log\LoggerInterface
+ */
+ protected function getLogger(): LoggerInterface
+ {
+ return $this->logger instanceof LoggerInterface ? $this->logger : new NullLogger();
+ }
+
+ /**
+ * @return \Psr\Http\Message\StreamFactoryInterface
+ */
+ protected function getStreamFactory(): StreamFactoryInterface
+ {
+ return $this->streamFactory instanceof StreamFactoryInterface
+ ? $this->streamFactory
+ : Psr17FactoryDiscovery::findStreamFactory();
+ }
+
+ /**
+ * @return \Psr\Http\Message\RequestFactoryInterface
+ */
+ protected function getRequestFactory(): RequestFactoryInterface
+ {
+ return $this->requestFactory instanceof RequestFactoryInterface
+ ? $this->requestFactory
+ : Psr17FactoryDiscovery::findRequestFactory();
+ }
+
+ /**
+ * @return \Psr\Http\Message\UriFactoryInterface
+ */
+ protected function getUriFactory(): UriFactoryInterface
+ {
+ return $this->uriFactory instanceof UriFactoryInterface
+ ? $this->uriFactory
+ : Psr17FactoryDiscovery::findUriFactory();
+ }
}
diff --git a/src/Component/Psr7/AppendStream.php b/src/Component/Psr7/AppendStream.php
deleted file mode 100644
index 44282a2..0000000
--- a/src/Component/Psr7/AppendStream.php
+++ /dev/null
@@ -1,312 +0,0 @@
-
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see http://help.retailcrm.ru
- */
-namespace RetailCrm\Component\Psr7;
-
-use InvalidArgumentException;
-use Psr\Http\Message\StreamInterface;
-use RuntimeException;
-
-/**
- * Class AppendStream
- *
- * @category AppendStream
- * @package RetailCrm\Component\Psr7
- * @author Michael Dowling
- * @author RetailDriver LLC
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see https://help.retailcrm.ru
- */
-class AppendStream implements StreamInterface
-{
- /** @var StreamInterface[] Streams being decorated */
- private $streams = [];
-
- /** @var bool */
- private $seekable = true;
-
- /** @var int */
- private $current = 0;
-
- /** @var int */
- private $pos = 0;
-
- /**
- * AppendStream constructor.
- *
- * @param StreamInterface[] $streams Streams to decorate. Each stream must
- * be readable.
- */
- public function __construct(array $streams = [])
- {
- foreach ($streams as $stream) {
- $this->addStream($stream);
- }
- }
-
- /**
- * @return string
- * @throws \Throwable
- */
- public function __toString(): string
- {
- try {
- $this->rewind();
-
- return $this->getContents();
- } catch (\Throwable $e) {
- if (\PHP_VERSION_ID >= 70400) {
- throw $e;
- }
-
- trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
- }
- }
-
- /**
- * Add a stream to the AppendStream
- *
- * @param StreamInterface $stream Stream to append. Must be readable.
- *
- * @throws \InvalidArgumentException if the stream is not readable
- */
- public function addStream(StreamInterface $stream): void
- {
- if (!$stream->isReadable()) {
- throw new InvalidArgumentException('Each stream must be readable');
- }
-
- // The stream is only seekable if all streams are seekable
- if (!$stream->isSeekable()) {
- $this->seekable = false;
- }
-
- $this->streams[] = $stream;
- }
-
- /**
- * @return string
- */
- public function getContents(): string
- {
- return Utils::copyToString($this);
- }
-
- /**
- * Closes each attached stream.
- */
- public function close(): void
- {
- $this->pos = 0;
- $this->current = 0;
- $this->seekable = true;
-
- foreach ($this->streams as $stream) {
- $stream->close();
- }
-
- $this->streams = [];
- }
-
- /**
- * Detaches each attached stream.
- *
- * Returns null as it's not clear which underlying stream resource to return.
- */
- public function detach()
- {
- $this->pos = $this->current = 0;
- $this->seekable = true;
-
- foreach ($this->streams as $stream) {
- $stream->detach();
- }
-
- $this->streams = [];
-
- return null;
- }
-
- /**
- * @return int
- */
- public function tell(): int
- {
- return $this->pos;
- }
-
- /**
- * Tries to calculate the size by adding the size of each stream.
- *
- * If any of the streams do not return a valid number, then the size of the
- * append stream cannot be determined and null is returned.
- */
- public function getSize(): ?int
- {
- $size = 0;
-
- foreach ($this->streams as $stream) {
- $streamSize = $stream->getSize();
-
- if ($streamSize === null) {
- return null;
- }
-
- $size += $streamSize;
- }
-
- return $size;
- }
-
- /**
- * @return bool
- */
- public function eof(): bool
- {
- return !$this->streams ||
- ($this->current >= count($this->streams) - 1 &&
- $this->streams[$this->current]->eof());
- }
-
- /**
- *
- */
- public function rewind(): void
- {
- $this->seek(0);
- }
-
- /**
- * Attempts to seek to the given position. Only supports SEEK_SET.
- *
- * @param int $offset
- * @param int $whence
- */
- public function seek($offset, $whence = SEEK_SET): void
- {
- if (!$this->seekable) {
- throw new RuntimeException('This AppendStream is not seekable');
- }
-
- if ($whence !== SEEK_SET) {
- throw new RuntimeException('The AppendStream can only seek with SEEK_SET');
- }
-
- $this->pos = $this->current = 0;
-
- // Rewind each stream
- foreach ($this->streams as $index => $stream) {
- try {
- $stream->rewind();
- } catch (\Exception $exception) {
- throw new RuntimeException('Unable to seek stream '
- . $index . ' of the AppendStream', 0, $exception);
- }
- }
-
- // Seek to the actual position by reading from each stream
- while ($this->pos < $offset && !$this->eof()) {
- $result = $this->read(min(8096, $offset - $this->pos));
-
- if ($result === '') {
- break;
- }
- }
- }
-
- /**
- * Reads from all of the appended streams until the length is met or EOF.
- *
- * @param int $length
- *
- * @return string
- */
- public function read($length): string
- {
- $buffer = '';
- $total = count($this->streams) - 1;
- $remaining = $length;
- $progressToNext = false;
-
- while ($remaining > 0) {
- if ($progressToNext || $this->streams[$this->current]->eof()) {
- $progressToNext = false;
-
- if ($this->current === $total) {
- break;
- }
-
- $this->current++;
- }
-
- $result = $this->streams[$this->current]->read($remaining);
-
- if ($result === '') {
- $progressToNext = true;
-
- continue;
- }
-
- $buffer .= $result;
- $remaining = $length - strlen($buffer);
- }
-
- $this->pos += strlen($buffer);
-
- return $buffer;
- }
-
- /**
- * @return bool
- */
- public function isReadable(): bool
- {
- return true;
- }
-
- /**
- * @return bool
- */
- public function isWritable(): bool
- {
- return false;
- }
-
- /**
- * @return bool
- */
- public function isSeekable(): bool
- {
- return $this->seekable;
- }
-
- /**
- * @param string $string
- *
- * @return int
- */
- public function write($string): int
- {
- throw new RuntimeException('Cannot write to an AppendStream');
- }
-
- /**
- * @param null $key
- *
- * @return array|mixed|null
- */
- public function getMetadata($key = null)
- {
- return $key ? null : [];
- }
-}
diff --git a/src/Component/Psr7/BufferStream.php b/src/Component/Psr7/BufferStream.php
deleted file mode 100644
index 2e822de..0000000
--- a/src/Component/Psr7/BufferStream.php
+++ /dev/null
@@ -1,198 +0,0 @@
-
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see http://help.retailcrm.ru
- */
-namespace RetailCrm\Component\Psr7;
-
-use Psr\Http\Message\StreamInterface;
-use RuntimeException;
-
-/**
- * Class BufferStream
- *
- * @category BufferStream
- * @package RetailCrm\Component\Psr7
- * @author Michael Dowling
- * @author RetailDriver LLC
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see https://help.retailcrm.ru
- */
-class BufferStream implements StreamInterface
-{
- /** @var int */
- private $hwm;
-
- /** @var string */
- private $buffer = '';
-
- /**
- * @param int $hwm High water mark, representing the preferred maximum
- * buffer size. If the size of the buffer exceeds the high
- * water mark, then calls to write will continue to succeed
- * but will return 0 to inform writers to slow down
- * until the buffer has been drained by reading from it.
- */
- public function __construct(int $hwm = 16384)
- {
- $this->hwm = $hwm;
- }
-
- /**
- * @return string
- */
- public function __toString(): string
- {
- return $this->getContents();
- }
-
- /**
- * @return string
- */
- public function getContents(): string
- {
- $buffer = $this->buffer;
- $this->buffer = '';
-
- return $buffer;
- }
-
- /**
- *
- */
- public function close(): void
- {
- $this->buffer = '';
- }
-
- /**
- * @return |null
- */
- public function detach()
- {
- $this->close();
-
- return null;
- }
-
- /**
- * @return int|null
- */
- public function getSize(): ?int
- {
- return strlen($this->buffer);
- }
-
- /**
- * @return bool
- */
- public function isReadable(): bool
- {
- return true;
- }
-
- /**
- * @return bool
- */
- public function isWritable(): bool
- {
- return true;
- }
-
- /**
- * @return bool
- */
- public function isSeekable(): bool
- {
- return false;
- }
-
- /**
- *
- */
- public function rewind(): void
- {
- $this->seek(0);
- }
-
- /**
- * @param $offset
- * @param int $whence
- */
- public function seek($offset, $whence = SEEK_SET): void
- {
- throw new RuntimeException('Cannot seek a BufferStream');
- }
-
- /**
- * @return bool
- */
- public function eof(): bool
- {
- return $this->buffer === '';
- }
-
- /**
- * @return int
- */
- public function tell(): int
- {
- throw new RuntimeException('Cannot determine the position of a BufferStream');
- }
-
- /**
- * Reads data from the buffer.
- */
- public function read($length): string
- {
- $currentLength = strlen($this->buffer);
-
- if ($length >= $currentLength) {
- // No need to slice the buffer because we don't have enough data.
- $result = $this->buffer;
- $this->buffer = '';
- } else {
- // Slice up the result to provide a subset of the buffer.
- $result = substr($this->buffer, 0, $length);
- $this->buffer = substr($this->buffer, $length);
- }
-
- return $result;
- }
-
- /**
- * Writes data to the buffer.
- */
- public function write($string): int
- {
- $this->buffer .= $string;
-
- if (strlen($this->buffer) >= $this->hwm) {
- return 0;
- }
-
- return strlen($string);
- }
-
- /**
- * @param null $key
- *
- * @return array|int|null
- */
- public function getMetadata($key = null)
- {
- if ($key === 'hwm') {
- return $this->hwm;
- }
-
- return $key ? null : [];
- }
-}
diff --git a/src/Component/Psr7/MultipartStream.php b/src/Component/Psr7/MultipartStream.php
deleted file mode 100644
index 926631a..0000000
--- a/src/Component/Psr7/MultipartStream.php
+++ /dev/null
@@ -1,391 +0,0 @@
-
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see http://help.retailcrm.ru
- */
-namespace RetailCrm\Component\Psr7;
-
-use InvalidArgumentException;
-use Psr\Http\Message\StreamInterface;
-use RuntimeException;
-use UnexpectedValueException;
-
-/**
- * Class MultipartStream
- *
- * @category MultipartStream
- * @package RetailCrm\Component\Psr7
- * @author Michael Dowling
- * @author RetailDriver LLC
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see https://help.retailcrm.ru
- */
-class MultipartStream implements StreamInterface
-{
- /**
- * @var \Psr\Http\Message\StreamInterface $stream
- */
- private $stream;
-
- /** @var string */
- private $boundary;
-
- /**
- * @param array $elements Array of associative arrays, each containing a
- * required "name" key mapping to the form field,
- * name, a required "contents" key mapping to a
- * StreamInterface/resource/string, an optional
- * "headers" associative array of custom headers,
- * and an optional "filename" key mapping to a
- * string to send as the filename in the part.
- * @param string|null $boundary You can optionally provide a specific boundary
- *
- */
- public function __construct(array $elements = [], string $boundary = null)
- {
- $this->boundary = $boundary ?: sha1(uniqid('', true));
- $this->stream = $this->createStream($elements);
- }
-
- /**
- * Magic method used to create a new stream if streams are not added in
- * the constructor of a decorator.
- *
- * @param string $name
- *
- * @return StreamInterface
- */
- public function __get(string $name)
- {
- if ($name === 'stream') {
- $this->stream = $this->createStream();
-
- return $this->stream;
- }
-
- throw new UnexpectedValueException("$name not found on class");
- }
-
- /**
- * @param string $name
- * @param $value
- */
- public function __set(string $name, $value)
- {
- throw new RuntimeException('Not implemented');
- }
-
- /**
- * @param string $name
- */
- public function __isset(string $name)
- {
- throw new RuntimeException('Not implemented');
- }
-
- /**
- * @return string
- * @throws \Throwable
- */
- public function __toString(): string
- {
- try {
- if ($this->isSeekable()) {
- $this->seek(0);
- }
-
- return $this->getContents();
- } catch (\Throwable $e) {
- if (\PHP_VERSION_ID >= 70400) {
- throw $e;
- }
-
- trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
- }
- }
-
- /**
- * Allow decorators to implement custom methods
- *
- * @param string $method
- * @param array $args
- *
- * @return mixed
- */
- public function __call(string $method, array $args)
- {
- /** @var callable $callable */
- $callable = [$this->stream, $method];
- $result = call_user_func_array($callable, $args);
-
- return $result === $this->stream ? $this : $result;
- }
-
- /**
- * @return string
- */
- public function getContents(): string
- {
- return Utils::copyToString($this);
- }
-
- /**
- * close
- */
- public function close(): void
- {
- $this->stream->close();
- }
-
- /**
- * @param null $key
- *
- * @return array|mixed|null
- */
- public function getMetadata($key = null)
- {
- return $this->stream->getMetadata($key);
- }
-
- /**
- * @return resource|null
- */
- public function detach()
- {
- return $this->stream->detach();
- }
-
- /**
- * @return int|null
- */
- public function getSize(): ?int
- {
- return $this->stream->getSize();
- }
-
- /**
- * @return bool
- */
- public function eof(): bool
- {
- return $this->stream->eof();
- }
-
- /**
- * @return int
- */
- public function tell(): int
- {
- return $this->stream->tell();
- }
-
- /**
- * @return bool
- */
- public function isReadable(): bool
- {
- return $this->stream->isReadable();
- }
-
- /**
- * @return bool
- */
- public function isWritable(): bool
- {
- return $this->stream->isWritable();
- }
-
- /**
- * @return bool
- */
- public function isSeekable(): bool
- {
- return $this->stream->isSeekable();
- }
-
- /**
- * rewind
- */
- public function rewind(): void
- {
- $this->seek(0);
- }
-
- /**
- * @param int $offset
- * @param int $whence
- */
- public function seek($offset, $whence = SEEK_SET): void
- {
- $this->stream->seek($offset, $whence);
- }
-
- /**
- * @param int $length
- *
- * @return string
- */
- public function read($length): string
- {
- return $this->stream->read($length);
- }
-
- /**
- * @param string $string
- *
- * @return int
- */
- public function write($string): int
- {
- return $this->stream->write($string);
- }
-
- /**
- * @return string
- */
- public function getBoundary(): string
- {
- return $this->boundary;
- }
-
- /**
- * Create the aggregate stream that will be used to upload the POST data
- *
- * @param array $elements
- *
- * @return \Psr\Http\Message\StreamInterface
- */
- protected function createStream(array $elements = []): StreamInterface
- {
- $stream = new AppendStream();
-
- foreach ($elements as $element) {
- $this->addElement($stream, $element);
- }
-
- // Add the trailing boundary with CRLF
- $stream->addStream(Utils::streamFor("--{$this->boundary}--\r\n"));
-
- return $stream;
- }
-
- /**
- * Get the headers needed before transferring the content of a POST file
- *
- * @param array $headers
- *
- * @return string
- */
- private function getHeaders(array $headers): string
- {
- $str = '';
-
- foreach ($headers as $key => $value) {
- $str .= "{$key}: {$value}\r\n";
- }
-
- return "--{$this->boundary}\r\n" . trim($str) . "\r\n\r\n";
- }
-
- /**
- * @param \RetailCrm\Component\Psr7\AppendStream $stream
- * @param array $element
- */
- private function addElement(AppendStream $stream, array $element): void
- {
- foreach (['contents', 'name'] as $key) {
- if (!array_key_exists($key, $element)) {
- throw new InvalidArgumentException("A '{$key}' key is required");
- }
- }
-
- $element['contents'] = Utils::streamFor($element['contents']);
-
- if (empty($element['filename'])) {
- $uri = $element['contents']->getMetadata('uri');
-
- if (substr($uri, 0, 6) !== 'php://') {
- $element['filename'] = $uri;
- }
- }
-
- [$body, $headers] = $this->createElement(
- $element['name'],
- $element['contents'],
- $element['filename'] ?? null,
- $element['headers'] ?? []
- );
-
- $stream->addStream(Utils::streamFor($this->getHeaders($headers)));
- $stream->addStream($body);
- $stream->addStream(Utils::streamFor("\r\n"));
- }
-
- /**
- * @param string $name
- * @param \Psr\Http\Message\StreamInterface $stream
- * @param string|null $filename
- * @param array $headers
- *
- * @return array
- */
- private function createElement(string $name, StreamInterface $stream, ?string $filename, array $headers): array
- {
- // Set a default content-disposition header if one was no provided
- $disposition = $this->getHeader($headers, 'content-disposition');
-
- if (!$disposition) {
- $headers['Content-Disposition'] = ($filename === '0' || $filename)
- ? sprintf(
- 'form-data; name="%s"; filename="%s"',
- $name,
- basename($filename)
- )
- : "form-data; name=\"{$name}\"";
- }
-
- // Set a default content-length header if one was no provided
- $length = $this->getHeader($headers, 'content-length');
-
- if (!$length) {
- $length = $stream->getSize();
- $headers['Content-Length'] = (string) $length;
- }
-
- // Set a default Content-Type if one was not supplied
- $type = $this->getHeader($headers, 'content-type');
-
- if (!$type && ($filename === '0' || $filename)) {
- $type = Utils::mimetypeFromFilename($filename);
- $headers['Content-Type'] = $type;
- }
-
- return [$stream, $headers];
- }
-
- /**
- * @param array $headers
- * @param string $key
- *
- * @return mixed|null
- */
- private function getHeader(array $headers, string $key)
- {
- $lowercaseHeader = strtolower($key);
- foreach ($headers as $k => $v) {
- if (strtolower($k) === $lowercaseHeader) {
- return $v;
- }
- }
-
- return null;
- }
-}
diff --git a/src/Component/Psr7/PumpStream.php b/src/Component/Psr7/PumpStream.php
deleted file mode 100644
index 2bbc231..0000000
--- a/src/Component/Psr7/PumpStream.php
+++ /dev/null
@@ -1,242 +0,0 @@
-
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see http://help.retailcrm.ru
- */
-namespace RetailCrm\Component\Psr7;
-
-use Psr\Http\Message\StreamInterface;
-use RuntimeException;
-
-/**
- * Class PumpStream
- *
- * @category PumpStream
- * @package RetailCrm\Component\Psr7
- * @author Michael Dowling
- * @author RetailDriver LLC
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see https://help.retailcrm.ru
- */
-class PumpStream implements StreamInterface
-{
- /** @var callable|null */
- private $source;
-
- /** @var int|null */
- private $size;
-
- /** @var int */
- private $tellPos = 0;
-
- /** @var array */
- private $metadata;
-
- /** @var BufferStream */
- private $buffer;
-
- /**
- * @param callable(int): (string|null|false) $source Source of the stream data. The callable MAY
- * accept an integer argument used to control the
- * amount of data to return. The callable MUST
- * return a string when called, or false|null on error
- * or EOF.
- * @param array{size?: int, metadata?: array} $options Stream options:
- * - metadata: Hash of metadata to use with stream.
- * - size: Size of the stream, if known.
- */
- public function __construct(callable $source, array $options = [])
- {
- $this->source = $source;
- $this->size = $options['size'] ?? null;
- $this->metadata = $options['metadata'] ?? [];
- $this->buffer = new BufferStream();
- }
-
- /**
- * @return string
- * @throws \Throwable
- */
- public function __toString(): string
- {
- try {
- return Utils::copyToString($this);
- } catch (\Throwable $e) {
- if (\PHP_VERSION_ID >= 70400) {
- throw $e;
- }
-
- trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR);
- }
- }
-
- /**
- * @return void
- */
- public function close(): void
- {
- $this->detach();
- }
-
- /**
- * @return |null
- */
- public function detach()
- {
- $this->tellPos = 0;
- $this->source = null;
-
- return null;
- }
-
- /**
- * @return int|null
- */
- public function getSize(): ?int
- {
- return $this->size;
- }
-
- /**
- * @return int
- */
- public function tell(): int
- {
- return $this->tellPos;
- }
-
- /**
- * @return bool
- */
- public function eof(): bool
- {
- return $this->source === null;
- }
-
- /**
- * @return bool
- */
- public function isSeekable(): bool
- {
- return false;
- }
-
- /**
- *
- */
- public function rewind(): void
- {
- $this->seek(0);
- }
-
- /**
- * @param $offset
- * @param int $whence
- */
- public function seek($offset, $whence = SEEK_SET): void
- {
- throw new RuntimeException('Cannot seek a PumpStream');
- }
-
- /**
- * @return bool
- */
- public function isWritable(): bool
- {
- return false;
- }
-
- /**
- * @param $string
- *
- * @return int
- */
- public function write($string): int
- {
- throw new RuntimeException('Cannot write to a PumpStream');
- }
-
- /**
- * @return bool
- */
- public function isReadable(): bool
- {
- return true;
- }
-
- /**
- * @param $length
- *
- * @return string
- */
- public function read($length): string
- {
- $data = $this->buffer->read($length);
- $readLen = strlen($data);
- $this->tellPos += $readLen;
- $remaining = $length - $readLen;
-
- if ($remaining) {
- $this->pump($remaining);
- $data .= $this->buffer->read($remaining);
- $this->tellPos += strlen($data) - $readLen;
- }
-
- return $data;
- }
-
- /**
- * @return string
- */
- public function getContents(): string
- {
- $result = '';
- while (!$this->eof()) {
- $result .= $this->read(1000000);
- }
-
- return $result;
- }
-
- /**
- * @param null $key
- *
- * @return array|mixed|null
- */
- public function getMetadata($key = null)
- {
- if (!$key) {
- return $this->metadata;
- }
-
- return $this->metadata[$key] ?? null;
- }
-
- /**
- * @param int $length
- */
- private function pump(int $length): void
- {
- if ($this->source) {
- do {
- $data = call_user_func($this->source, $length);
-
- if ($data === false || $data === null) {
- $this->source = null;
- return;
- }
-
- $this->buffer->write($data);
- $length -= strlen($data);
- } while ($length > 0);
- }
- }
-}
diff --git a/src/Component/Psr7/Stream.php b/src/Component/Psr7/Stream.php
deleted file mode 100644
index e150758..0000000
--- a/src/Component/Psr7/Stream.php
+++ /dev/null
@@ -1,375 +0,0 @@
-
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see http://help.retailcrm.ru
- */
-namespace RetailCrm\Component\Psr7;
-
-use InvalidArgumentException;
-use Psr\Http\Message\StreamInterface;
-use RuntimeException;
-
-/**
- * Class Stream
- *
- * @category Stream
- * @package RetailCrm\Component\Psr7
- * @author Michael Dowling
- * @author RetailDriver LLC
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see https://help.retailcrm.ru
- * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
- */
-class Stream implements StreamInterface
-{
- /**
- * @see http://php.net/manual/function.fopen.php
- * @see http://php.net/manual/en/function.gzopen.php
- */
- private const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/';
- /**
- *
- */
- private const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/';
-
- /** @var resource */
- private $stream;
-
- /** @var int|null */
- private $size;
-
- /** @var bool */
- private $seekable;
-
- /** @var bool */
- private $readable;
-
- /** @var bool */
- private $writable;
-
- /** @var string|null */
- private $uri;
-
- /** @var mixed[] */
- private $customMetadata;
-
- /**
- * This constructor accepts an associative array of options.
- *
- * - size: (int) If a read stream would otherwise have an indeterminate
- * size, but the size is known due to foreknowledge, then you can
- * provide that size, in bytes.
- * - metadata: (array) Any additional metadata to return when the metadata
- * of the stream is accessed.
- *
- * @param resource $stream Stream resource to wrap.
- * @param array{size?: int, metadata?: array} $options Associative array of options.
- *
- * @throws \InvalidArgumentException if the stream is not a stream resource
- */
- public function __construct($stream, array $options = [])
- {
- if (!is_resource($stream)) {
- throw new InvalidArgumentException('Stream must be a resource');
- }
-
- if (isset($options['size'])) {
- $this->size = $options['size'];
- }
-
- $this->customMetadata = $options['metadata'] ?? [];
- $this->stream = $stream;
- $meta = stream_get_meta_data($this->stream);
- $this->seekable = $meta['seekable'];
- $this->readable = (bool)preg_match(self::READABLE_MODES, $meta['mode']);
- $this->writable = (bool)preg_match(self::WRITABLE_MODES, $meta['mode']);
- $this->uri = $this->getMetadata('uri');
- }
-
- /**
- * Closes the stream when the destructed
- */
- public function __destruct()
- {
- $this->close();
- }
-
- /**
- * @return string
- * @throws \Throwable
- */
- public function __toString(): string
- {
- try {
- if ($this->isSeekable()) {
- $this->seek(0);
- }
-
- return $this->getContents();
- } catch (\Throwable $exception) {
- if (\PHP_VERSION_ID >= 70400) {
- throw $exception;
- }
-
- trigger_error(
- sprintf(
- '%s::__toString exception: %s',
- self::class,
- (string) $exception
- ),
- E_USER_ERROR
- );
- }
- }
-
- /**
- * @return string
- */
- public function getContents(): string
- {
- if (!isset($this->stream)) {
- throw new RuntimeException('Stream is detached');
- }
-
- $contents = stream_get_contents($this->stream);
-
- if ($contents === false) {
- throw new RuntimeException('Unable to read stream contents');
- }
-
- return $contents;
- }
-
- /**
- *
- */
- public function close(): void
- {
- if (isset($this->stream)) {
- if (is_resource($this->stream)) {
- fclose($this->stream);
- }
-
- $this->detach();
- }
- }
-
- /**
- * @return resource|null
- */
- public function detach()
- {
- if (!isset($this->stream)) {
- return null;
- }
-
- $result = $this->stream;
-
- unset($this->stream);
-
- $this->uri = null;
- $this->size = null;
- $this->readable = false;
- $this->writable = false;
- $this->seekable = false;
-
- return $result;
- }
-
- /**
- * @return int|null
- */
- public function getSize(): ?int
- {
- if ($this->size !== null) {
- return $this->size;
- }
-
- if (!isset($this->stream)) {
- return null;
- }
-
- // Clear the stat cache if the stream has a URI
- if ($this->uri) {
- clearstatcache(true, $this->uri);
- }
-
- $stats = fstat($this->stream);
- if (is_array($stats) && isset($stats['size'])) {
- $this->size = $stats['size'];
- return $this->size;
- }
-
- return null;
- }
-
- /**
- * @return bool
- */
- public function isReadable(): bool
- {
- return $this->readable;
- }
-
- /**
- * @return bool
- */
- public function isWritable(): bool
- {
- return $this->writable;
- }
-
- /**
- * @return bool
- */
- public function isSeekable(): bool
- {
- return $this->seekable;
- }
-
- /**
- * @return bool
- */
- public function eof(): bool
- {
- if (!isset($this->stream)) {
- throw new RuntimeException('Stream is detached');
- }
-
- return feof($this->stream);
- }
-
- /**
- * @return int
- */
- public function tell(): int
- {
- if (!isset($this->stream)) {
- throw new RuntimeException('Stream is detached');
- }
-
- $result = ftell($this->stream);
-
- if ($result === false) {
- throw new RuntimeException('Unable to determine stream position');
- }
-
- return $result;
- }
-
- /**
- *
- */
- public function rewind(): void
- {
- $this->seek(0);
- }
-
- /**
- * @param int $offset
- * @param int $whence
- */
- public function seek($offset, $whence = SEEK_SET): void
- {
- $whence = (int) $whence;
-
- if (!isset($this->stream)) {
- throw new RuntimeException('Stream is detached');
- }
-
- if (!$this->seekable) {
- throw new RuntimeException('Stream is not seekable');
- }
-
- if (fseek($this->stream, $offset, $whence) === -1) {
- throw new RuntimeException('Unable to seek to stream position '
- . $offset . ' with whence ' . var_export($whence, true));
- }
- }
-
- /**
- * @param int $length
- *
- * @return string
- */
- public function read($length): string
- {
- if (!isset($this->stream)) {
- throw new RuntimeException('Stream is detached');
- }
-
- if (!$this->readable) {
- throw new RuntimeException('Cannot read from non-readable stream');
- }
-
- if ($length < 0) {
- throw new RuntimeException('Length parameter cannot be negative');
- }
-
- if (0 === $length) {
- return '';
- }
-
- $string = fread($this->stream, $length);
-
- if (false === $string) {
- throw new RuntimeException('Unable to read from stream');
- }
-
- return $string;
- }
-
- /**
- * @param string $string
- *
- * @return int
- */
- public function write($string): int
- {
- if (!isset($this->stream)) {
- throw new RuntimeException('Stream is detached');
- }
-
- if (!$this->writable) {
- throw new RuntimeException('Cannot write to a non-writable stream');
- }
-
- // We can't know the size after writing anything
- $this->size = null;
- $result = fwrite($this->stream, $string);
-
- if ($result === false) {
- throw new RuntimeException('Unable to write to stream');
- }
-
- return $result;
- }
-
- /**
- * @param null $key
- *
- * @return array|mixed|mixed[]|null
- */
- public function getMetadata($key = null)
- {
- if (!isset($this->stream)) {
- return $key ? null : [];
- } elseif (!$key) {
- return $this->customMetadata + stream_get_meta_data($this->stream);
- } elseif (isset($this->customMetadata[$key])) {
- return $this->customMetadata[$key];
- }
-
- $meta = stream_get_meta_data($this->stream);
-
- return $meta[$key] ?? null;
- }
-}
diff --git a/src/Component/Psr7/Utils.php b/src/Component/Psr7/Utils.php
deleted file mode 100644
index 95f3be9..0000000
--- a/src/Component/Psr7/Utils.php
+++ /dev/null
@@ -1,308 +0,0 @@
-
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see http://help.retailcrm.ru
- */
-namespace RetailCrm\Component\Psr7;
-
-use InvalidArgumentException;
-use Psr\Http\Message\StreamInterface;
-use RuntimeException;
-
-/**
- * Class Utils
- *
- * @category Utils
- * @package RetailCrm\Component\Psr7
- * @author Michael Dowling
- * @author RetailDriver LLC
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see https://help.retailcrm.ru
- */
-class Utils
-{
- /**
- * Copy the contents of a stream into a string until the given number of
- * bytes have been read.
- *
- * @param StreamInterface $stream Stream to read
- * @param int $maxLen Maximum number of bytes to read. Pass -1
- * to read the entire stream.
- *
- * @return string
- * @throws \RuntimeException on error.
- */
- public static function copyToString(StreamInterface $stream, int $maxLen = -1): string
- {
- $buffer = '';
-
- if ($maxLen === -1) {
- while (!$stream->eof()) {
- $buf = $stream->read(1048576);
-
- if ($buf === '') {
- break;
- }
-
- $buffer .= $buf;
- }
- return $buffer;
- }
-
- $len = 0;
-
- while (!$stream->eof() && $len < $maxLen) {
- $buf = $stream->read($maxLen - $len);
- if ($buf === '') {
- break;
- }
- $buffer .= $buf;
- $len = strlen($buffer);
- }
-
- return $buffer;
- }
-
- /**
- * Safely opens a PHP stream resource using a filename.
- *
- * When fopen fails, PHP normally raises a warning. This function adds an
- * error handler that checks for errors and throws an exception instead.
- *
- * @param string $filename File to open
- * @param string $mode Mode used to open the file
- *
- * @return resource
- *
- * @throws \RuntimeException if the file cannot be opened
- */
- public static function tryFopen(string $filename, string $mode)
- {
- $ex = null;
-
- set_error_handler(static function (int $errno, string $errstr) use ($filename, $mode, &$ex): bool {
- $ex = new RuntimeException(sprintf(
- 'Unable to open %s using mode %s: %s',
- $filename,
- $mode,
- $errstr
- ));
-
- return false;
- });
-
- /** @var resource $handle */
- $handle = fopen($filename, $mode);
-
- restore_error_handler();
-
- if ($ex) {
- throw $ex;
- }
-
- return $handle;
- }
-
- /**
- * Create a new stream based on the input type.
- *
- * Options is an associative array that can contain the following keys:
- * - metadata: Array of custom metadata.
- * - size: Size of the stream.
- *
- * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
- * @param array{size?: int, metadata?: array} $options Additional options
- *
- * @return \Psr\Http\Message\StreamInterface
- * @throws \InvalidArgumentException if the $resource arg is not valid.
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
- */
- public static function streamFor($resource = '', array $options = []): StreamInterface
- {
- if (is_scalar($resource)) {
- $stream = self::tryFopen('php://temp', 'r+');
-
- if ($resource !== '') {
- fwrite($stream, (string) $resource);
- fseek($stream, 0);
- }
-
- return new Stream($stream, $options);
- }
-
- switch (gettype($resource)) {
- case 'resource':
- /** @var resource $resource */
- return new Stream($resource, $options);
- case 'object':
- /** @var object $resource */
- if ($resource instanceof StreamInterface) {
- return $resource;
- } elseif ($resource instanceof \Iterator) {
- return new PumpStream(function () use ($resource) {
- if (!$resource->valid()) {
- return false;
- }
-
- $result = $resource->current();
- $resource->next();
-
- return $result;
- }, $options);
- } elseif (method_exists($resource, '__toString')) {
- return self::streamFor((string) $resource, $options);
- }
-
- break;
- case 'NULL':
- return new Stream(self::tryFopen('php://temp', 'r+'), $options);
- }
-
- if (is_callable($resource)) {
- return new PumpStream($resource, $options);
- }
-
- throw new InvalidArgumentException('Invalid resource type: ' . gettype($resource));
- }
-
- /**
- * Determines the mimetype of a file by looking at its extension.
- *
- * @param string $filename
- *
- * @return string|null
- */
- public static function mimetypeFromFilename(string $filename): ?string
- {
- return self::mimetypeFromExtension(pathinfo($filename, PATHINFO_EXTENSION));
- }
-
- /**
- * Maps a file extensions to a mimetype.
- *
- * @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types
- *
- * @param string $extension
- *
- * @return string|null
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
- */
- public static function mimetypeFromExtension(string $extension): ?string
- {
- $mimetypes = [
- '3gp' => 'video/3gpp',
- '7z' => 'application/x-7z-compressed',
- 'aac' => 'audio/x-aac',
- 'ai' => 'application/postscript',
- 'aif' => 'audio/x-aiff',
- 'asc' => 'text/plain',
- 'asf' => 'video/x-ms-asf',
- 'atom' => 'application/atom+xml',
- 'avi' => 'video/x-msvideo',
- 'bmp' => 'image/bmp',
- 'bz2' => 'application/x-bzip2',
- 'cer' => 'application/pkix-cert',
- 'crl' => 'application/pkix-crl',
- 'crt' => 'application/x-x509-ca-cert',
- 'css' => 'text/css',
- 'csv' => 'text/csv',
- 'cu' => 'application/cu-seeme',
- 'deb' => 'application/x-debian-package',
- 'doc' => 'application/msword',
- 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- 'dvi' => 'application/x-dvi',
- 'eot' => 'application/vnd.ms-fontobject',
- 'eps' => 'application/postscript',
- 'epub' => 'application/epub+zip',
- 'etx' => 'text/x-setext',
- 'flac' => 'audio/flac',
- 'flv' => 'video/x-flv',
- 'gif' => 'image/gif',
- 'gz' => 'application/gzip',
- 'htm' => 'text/html',
- 'html' => 'text/html',
- 'ico' => 'image/x-icon',
- 'ics' => 'text/calendar',
- 'ini' => 'text/plain',
- 'iso' => 'application/x-iso9660-image',
- 'jar' => 'application/java-archive',
- 'jpe' => 'image/jpeg',
- 'jpeg' => 'image/jpeg',
- 'jpg' => 'image/jpeg',
- 'js' => 'text/javascript',
- 'json' => 'application/json',
- 'latex' => 'application/x-latex',
- 'log' => 'text/plain',
- 'm4a' => 'audio/mp4',
- 'm4v' => 'video/mp4',
- 'mid' => 'audio/midi',
- 'midi' => 'audio/midi',
- 'mov' => 'video/quicktime',
- 'mkv' => 'video/x-matroska',
- 'mp3' => 'audio/mpeg',
- 'mp4' => 'video/mp4',
- 'mp4a' => 'audio/mp4',
- 'mp4v' => 'video/mp4',
- 'mpe' => 'video/mpeg',
- 'mpeg' => 'video/mpeg',
- 'mpg' => 'video/mpeg',
- 'mpg4' => 'video/mp4',
- 'oga' => 'audio/ogg',
- 'ogg' => 'audio/ogg',
- 'ogv' => 'video/ogg',
- 'ogx' => 'application/ogg',
- 'pbm' => 'image/x-portable-bitmap',
- 'pdf' => 'application/pdf',
- 'pgm' => 'image/x-portable-graymap',
- 'png' => 'image/png',
- 'pnm' => 'image/x-portable-anymap',
- 'ppm' => 'image/x-portable-pixmap',
- 'ppt' => 'application/vnd.ms-powerpoint',
- 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
- 'ps' => 'application/postscript',
- 'qt' => 'video/quicktime',
- 'rar' => 'application/x-rar-compressed',
- 'ras' => 'image/x-cmu-raster',
- 'rss' => 'application/rss+xml',
- 'rtf' => 'application/rtf',
- 'sgm' => 'text/sgml',
- 'sgml' => 'text/sgml',
- 'svg' => 'image/svg+xml',
- 'swf' => 'application/x-shockwave-flash',
- 'tar' => 'application/x-tar',
- 'tif' => 'image/tiff',
- 'tiff' => 'image/tiff',
- 'torrent' => 'application/x-bittorrent',
- 'ttf' => 'application/x-font-ttf',
- 'txt' => 'text/plain',
- 'wav' => 'audio/x-wav',
- 'webm' => 'video/webm',
- 'webp' => 'image/webp',
- 'wma' => 'audio/x-ms-wma',
- 'wmv' => 'video/x-ms-wmv',
- 'woff' => 'application/x-font-woff',
- 'wsdl' => 'application/wsdl+xml',
- 'xbm' => 'image/x-xbitmap',
- 'xls' => 'application/vnd.ms-excel',
- 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
- 'xml' => 'application/xml',
- 'xpm' => 'image/x-xpixmap',
- 'xwd' => 'image/x-xwindowdump',
- 'yaml' => 'text/yaml',
- 'yml' => 'text/yaml',
- 'zip' => 'application/zip',
- ];
-
- $extension = strtolower($extension);
-
- return $mimetypes[$extension] ?? null;
- }
-}
diff --git a/src/Factory/RequestFactory.php b/src/Factory/RequestFactory.php
deleted file mode 100644
index 8f628c5..0000000
--- a/src/Factory/RequestFactory.php
+++ /dev/null
@@ -1,188 +0,0 @@
-
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see http://help.retailcrm.ru
- */
-namespace RetailCrm\Factory;
-
-use JMS\Serializer\SerializerInterface;
-use Psr\Http\Message\RequestInterface;
-use RetailCrm\Component\Exception\FactoryException;
-use RetailCrm\Component\Psr7\MultipartStream;
-use RetailCrm\Interfaces\AppDataInterface;
-use RetailCrm\Interfaces\AuthenticatorInterface;
-use RetailCrm\Interfaces\FileItemInterface;
-use RetailCrm\Interfaces\RequestFactoryInterface;
-use RetailCrm\Interfaces\RequestSignerInterface;
-use RetailCrm\Interfaces\RequestTimestampProviderInterface;
-use RetailCrm\Model\Request\BaseRequest;
-use RetailCrm\Service\RequestDataFilter;
-use RetailCrm\Traits\ValidatorAwareTrait;
-use Shieldon\Psr7\Request;
-use Shieldon\Psr7\Uri;
-use Symfony\Component\Validator\Validator\ValidatorInterface;
-
-/**
- * Class RequestFactory
- *
- * @category RequestFactory
- * @package RetailCrm\Factory
- * @author RetailDriver LLC
- * @license MIT https://mit-license.org
- * @link http://retailcrm.ru
- * @see https://help.retailcrm.ru
- * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
- */
-class RequestFactory implements RequestFactoryInterface
-{
- use ValidatorAwareTrait;
-
- /**
- * @var \RetailCrm\Interfaces\RequestSignerInterface $signer
- */
- private $signer;
-
- /**
- * @var RequestDataFilter $filter
- */
- private $filter;
-
- /**
- * @var SerializerInterface|\JMS\Serializer\Serializer $serializer
- */
- private $serializer;
-
- /**
- * @var RequestTimestampProviderInterface $timestampProvider
- */
- private $timestampProvider;
-
- /**
- * RequestFactory constructor.
- *
- * @param \RetailCrm\Interfaces\RequestSignerInterface $signer
- * @param \RetailCrm\Service\RequestDataFilter $filter
- * @param \JMS\Serializer\SerializerInterface $serializer
- * @param \Symfony\Component\Validator\Validator\ValidatorInterface $validator
- * @param \RetailCrm\Interfaces\RequestTimestampProviderInterface $timestampProvider
- */
- public function __construct(
- RequestSignerInterface $signer,
- RequestDataFilter $filter,
- SerializerInterface $serializer,
- ValidatorInterface $validator,
- RequestTimestampProviderInterface $timestampProvider
- ) {
- $this->signer = $signer;
- $this->filter = $filter;
- $this->serializer = $serializer;
- $this->validator = $validator;
- $this->timestampProvider = $timestampProvider;
- }
-
- /**
- * @param \RetailCrm\Model\Request\BaseRequest $request
- * @param \RetailCrm\Interfaces\AppDataInterface $appData
- * @param \RetailCrm\Interfaces\AuthenticatorInterface $authenticator
- *
- * @return \Psr\Http\Message\RequestInterface
- * @throws \RetailCrm\Component\Exception\FactoryException
- * @throws \RetailCrm\Component\Exception\ValidationException
- */
- public function fromModel(
- BaseRequest $request,
- AppDataInterface $appData,
- AuthenticatorInterface $authenticator
- ): RequestInterface {
- $authenticator->authenticate($request);
- $this->signer->sign($request, $appData);
- $this->timestampProvider->provide($request);
- $this->validate($request);
-
- $requestData = $this->serializer->toArray($request);
- $requestHasBinaryData = $this->filter->hasBinaryFromRequestData($requestData);
-
- if (empty($requestData)) {
- throw new FactoryException('Empty request data');
- }
-
- if ($requestHasBinaryData) {
- return $this->makeMultipartRequest($appData->getServiceUrl(), $requestData);
- }
-
- $queryData = http_build_query($requestData);
-
- if ($queryData !== '') {
- $queryData = '?' . $queryData;
- }
-
- return new Request(
- 'GET',
- new Uri($appData->getServiceUrl() . $queryData),
- '',
- self::defaultHeaders()
- );
- }
-
- /**
- * @param string $endpoint
- * @param array $contents
- *
- * @return \Psr\Http\Message\RequestInterface
- */
- private function makeMultipartRequest(string $endpoint, array $contents): RequestInterface
- {
- $prepared = [];
-
- foreach ($contents as $param => $value) {
- if ($value instanceof FileItemInterface) {
- $prepared[] = [
- 'name' => $param,
- 'contents' => $value->getStream(),
- 'filename' => $value->getFileName()
- ];
- } else {
- $prepared[] = [
- 'name' => $param,
- 'contents' => $value
- ];
- }
- }
-
- return new Request(
- 'POST',
- new Uri($endpoint),
- new MultipartStream($prepared),
- self::defaultHeaders()
- );
- }
-
- private static function defaultHeaders(): array
- {
- return [
- 'HTTP_ACCEPT' => 'application/json,application/xml;q=0.9',
- 'HTTP_ACCEPT_CHARSET' => 'utf-8;q=0.7,*;q=0.3',
- 'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7',
- 'HTTP_USER_AGENT' => 'Mozilla/5.0 (compatible; TopSdk; +http://retailcrm.pro)',
- 'HTTP_HOST' => '127.0.0.1',
- 'QUERY_STRING' => '',
- 'REMOTE_ADDR' => '127.0.0.1',
- 'REQUEST_METHOD' => 'GET',
- 'REQUEST_SCHEME' => 'http',
- 'REQUEST_TIME' => time(),
- 'REQUEST_TIME_FLOAT' => microtime(true),
- 'REQUEST_URI' => '',
- 'SCRIPT_NAME' => '',
- 'SERVER_NAME' => 'localhost',
- 'SERVER_PORT' => 80,
- 'SERVER_PROTOCOL' => 'HTTP/1.1',
- ];
- }
-}
diff --git a/src/Factory/TopRequestFactory.php b/src/Factory/TopRequestFactory.php
new file mode 100644
index 0000000..9ceff8f
--- /dev/null
+++ b/src/Factory/TopRequestFactory.php
@@ -0,0 +1,214 @@
+
+ * @license MIT https://mit-license.org
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+namespace RetailCrm\Factory;
+
+use Http\Message\MultipartStream\MultipartStreamBuilder;
+use JMS\Serializer\SerializerInterface;
+use Psr\Http\Message\RequestFactoryInterface;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\StreamFactoryInterface;
+use Psr\Http\Message\UriFactoryInterface;
+use RetailCrm\Component\Exception\FactoryException;
+use RetailCrm\Interfaces\AppDataInterface;
+use RetailCrm\Interfaces\AuthenticatorInterface;
+use RetailCrm\Interfaces\FileItemInterface;
+use RetailCrm\Interfaces\TopRequestFactoryInterface;
+use RetailCrm\Model\Request\BaseRequest;
+use RetailCrm\Service\RequestDataFilter;
+use UnexpectedValueException;
+
+/**
+ * Class TopRequestFactory
+ *
+ * @category TopRequestFactory
+ * @package RetailCrm\Factory
+ * @author RetailDriver LLC
+ * @license MIT https://mit-license.org
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ */
+class TopRequestFactory implements TopRequestFactoryInterface
+{
+ /**
+ * @var RequestDataFilter $filter
+ */
+ private $filter;
+
+ /**
+ * @var SerializerInterface|\JMS\Serializer\Serializer $serializer
+ */
+ private $serializer;
+
+ /**
+ * @var StreamFactoryInterface $streamFactory
+ */
+ private $streamFactory;
+
+ /**
+ * @var \Psr\Http\Message\RequestFactoryInterface $requestFactory
+ */
+ private $requestFactory;
+
+ /**
+ * @var \Psr\Http\Message\UriFactoryInterface $uriFactory
+ */
+ private $uriFactory;
+
+ /**
+ * @param \RetailCrm\Service\RequestDataFilter $filter
+ *
+ * @return TopRequestFactory
+ */
+ public function setFilter(RequestDataFilter $filter): TopRequestFactory
+ {
+ $this->filter = $filter;
+ return $this;
+ }
+
+ /**
+ * @param \JMS\Serializer\Serializer|\JMS\Serializer\SerializerInterface $serializer
+ *
+ * @return TopRequestFactory
+ */
+ public function setSerializer($serializer): TopRequestFactory
+ {
+ $this->serializer = $serializer;
+ return $this;
+ }
+
+ /**
+ * @param \Psr\Http\Message\StreamFactoryInterface $streamFactory
+ *
+ * @return TopRequestFactory
+ */
+ public function setStreamFactory(StreamFactoryInterface $streamFactory): TopRequestFactory
+ {
+ $this->streamFactory = $streamFactory;
+ return $this;
+ }
+
+ /**
+ * @param \Psr\Http\Message\RequestFactoryInterface $requestFactory
+ *
+ * @return TopRequestFactory
+ */
+ public function setRequestFactory(RequestFactoryInterface $requestFactory): TopRequestFactory
+ {
+ $this->requestFactory = $requestFactory;
+ return $this;
+ }
+
+ /**
+ * @param \Psr\Http\Message\UriFactoryInterface $uriFactory
+ *
+ * @return TopRequestFactory
+ */
+ public function setUriFactory(UriFactoryInterface $uriFactory): TopRequestFactory
+ {
+ $this->uriFactory = $uriFactory;
+ return $this;
+ }
+
+ /**
+ * @param \RetailCrm\Model\Request\BaseRequest $request
+ * @param \RetailCrm\Interfaces\AppDataInterface $appData
+ * @param \RetailCrm\Interfaces\AuthenticatorInterface $authenticator
+ *
+ * @return \Psr\Http\Message\RequestInterface
+ * @throws \RetailCrm\Component\Exception\FactoryException
+ */
+ public function fromModel(
+ BaseRequest $request,
+ AppDataInterface $appData,
+ AuthenticatorInterface $authenticator
+ ): RequestInterface {
+ $requestData = $this->serializer->toArray($request);
+ $requestHasBinaryData = $this->filter->hasBinaryFromRequestData($requestData);
+
+ if (empty($requestData)) {
+ throw new FactoryException('Empty request data');
+ }
+
+ if ($requestHasBinaryData) {
+ return $this->makeMultipartRequest($appData->getServiceUrl(), $requestData);
+ }
+
+ $queryData = http_build_query($requestData);
+
+ return $this->requestFactory
+ ->createRequest(
+ 'GET',
+ $this->uriFactory->createUri($appData->getServiceUrl())->withQuery($queryData)
+ )->withHeader('Content-Type', 'application/x-www-form-urlencoded');
+ }
+
+ /**
+ * @param string $endpoint
+ * @param array $contents
+ *
+ * @return \Psr\Http\Message\RequestInterface
+ */
+ private function makeMultipartRequest(string $endpoint, array $contents): RequestInterface
+ {
+ $builder = new MultipartStreamBuilder($this->streamFactory);
+
+ foreach ($contents as $param => $value) {
+ if ($value instanceof FileItemInterface) {
+ $builder->addResource($param, $value->getStream(), ['filename' => $value->getFileName()]);
+ } else {
+ $casted = $this->castValue($value);
+
+ if (null !== $casted) {
+ $builder->addResource($param, $casted);
+ }
+ }
+ }
+
+ $stream = $builder->build();
+
+ if ($stream->isSeekable()) {
+ $stream->seek(0);
+ }
+
+ return $this->requestFactory
+ ->createRequest('POST', $this->uriFactory->createUri($endpoint))
+ ->withBody($stream)
+ ->withHeader('Content-Type', 'multipart/form-data; boundary="'.$builder->getBoundary().'"');
+ }
+
+ /**
+ * Cast any type to one supported by MultipartStreamBuilder. NULL should be ignored.
+ *
+ * @param mixed $value
+ *
+ * @return string|resource|null
+ */
+ private function castValue($value)
+ {
+ $type = gettype($value);
+
+ switch ($type) {
+ case 'resource':
+ case 'NULL':
+ return $value;
+ case 'boolean':
+ case 'integer':
+ case 'double':
+ case 'string':
+ return (string) $value;
+ default:
+ throw new UnexpectedValueException(sprintf('Got value with unsupported type: %s', $type));
+ }
+ }
+}
diff --git a/src/Interfaces/RequestFactoryInterface.php b/src/Interfaces/TopRequestFactoryInterface.php
similarity index 87%
rename from src/Interfaces/RequestFactoryInterface.php
rename to src/Interfaces/TopRequestFactoryInterface.php
index 0b5c2ba..36b9954 100644
--- a/src/Interfaces/RequestFactoryInterface.php
+++ b/src/Interfaces/TopRequestFactoryInterface.php
@@ -3,7 +3,7 @@
/**
* PHP version 7.3
*
- * @category RequestFactoryInterface
+ * @category TopRequestFactoryInterface
* @package RetailCrm\Interfaces
* @author RetailCRM
* @license MIT https://mit-license.org
@@ -17,16 +17,16 @@ use Psr\Http\Message\RequestInterface;
use RetailCrm\Model\Request\BaseRequest;
/**
- * Interface RequestFactoryInterface
+ * Interface TopRequestFactoryInterface
*
- * @category RequestFactoryInterface
+ * @category TopRequestFactoryInterface
* @package RetailCrm\Interfaces
* @author RetailDriver LLC
* @license MIT https://mit-license.org
* @link http://retailcrm.ru
* @see https://help.retailcrm.ru
*/
-interface RequestFactoryInterface
+interface TopRequestFactoryInterface
{
/**
* @param \RetailCrm\Model\Request\BaseRequest $request
diff --git a/src/Interfaces/TopRequestProcessorInterface.php b/src/Interfaces/TopRequestProcessorInterface.php
new file mode 100644
index 0000000..d8aff74
--- /dev/null
+++ b/src/Interfaces/TopRequestProcessorInterface.php
@@ -0,0 +1,47 @@
+
+ * @license MIT https://mit-license.org
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+
+namespace RetailCrm\Interfaces;
+
+use Psr\Http\Message\RequestInterface;
+use RetailCrm\Model\Request\BaseRequest;
+
+/**
+ * Interface TopRequestProcessorInterface
+ *
+ * @category TopRequestProcessorInterface
+ * @package RetailCrm\Interfaces
+ * @author RetailDriver LLC
+ * @license MIT https://mit-license.org
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ */
+interface TopRequestProcessorInterface
+{
+ /**
+ * Modifies request in order to prepare it for TOP API (timestamp, signature, etc).
+ *
+ * @param \RetailCrm\Model\Request\BaseRequest $request
+ * @param \RetailCrm\Interfaces\AppDataInterface $appData
+ * @param \RetailCrm\Interfaces\AuthenticatorInterface $authenticator
+ *
+ * @return void
+ * @throws \RetailCrm\Component\Exception\FactoryException
+ * @throws \RetailCrm\Component\Exception\ValidationException
+ */
+ public function process(
+ BaseRequest $request,
+ AppDataInterface $appData,
+ AuthenticatorInterface $authenticator
+ ): void;
+}
diff --git a/src/Service/TopRequestProcessor.php b/src/Service/TopRequestProcessor.php
new file mode 100644
index 0000000..4003022
--- /dev/null
+++ b/src/Service/TopRequestProcessor.php
@@ -0,0 +1,82 @@
+
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+namespace RetailCrm\Service;
+
+use RetailCrm\Interfaces\AppDataInterface;
+use RetailCrm\Interfaces\AuthenticatorInterface;
+use RetailCrm\Interfaces\RequestSignerInterface;
+use RetailCrm\Interfaces\RequestTimestampProviderInterface;
+use RetailCrm\Interfaces\TopRequestProcessorInterface;
+use RetailCrm\Model\Request\BaseRequest;
+use RetailCrm\Traits\ValidatorAwareTrait;
+
+/**
+ * Class TopRequestProcessor
+ *
+ * @category TopRequestProcessor
+ * @package RetailCrm\Service
+ * @author RetailDriver LLC
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ */
+class TopRequestProcessor implements TopRequestProcessorInterface
+{
+ use ValidatorAwareTrait;
+
+ /**
+ * @var \RetailCrm\Interfaces\RequestSignerInterface $signer
+ */
+ private $signer;
+
+ /**
+ * @var RequestTimestampProviderInterface $timestampProvider
+ */
+ private $timestampProvider;
+
+ /**
+ * @param \RetailCrm\Interfaces\RequestSignerInterface $signer
+ *
+ * @return TopRequestProcessor
+ */
+ public function setSigner(RequestSignerInterface $signer): TopRequestProcessor
+ {
+ $this->signer = $signer;
+ return $this;
+ }
+
+ /**
+ * @param \RetailCrm\Interfaces\RequestTimestampProviderInterface $timestampProvider
+ *
+ * @return TopRequestProcessor
+ */
+ public function setTimestampProvider(RequestTimestampProviderInterface $timestampProvider): TopRequestProcessor
+ {
+ $this->timestampProvider = $timestampProvider;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function process(
+ BaseRequest $request,
+ AppDataInterface $appData,
+ AuthenticatorInterface $authenticator
+ ): void {
+ $authenticator->authenticate($request);
+ $this->signer->sign($request, $appData);
+ $this->timestampProvider->provide($request);
+ $this->validate($request);
+ }
+}
diff --git a/src/TopClient/Client.php b/src/TopClient/Client.php
index 2983c93..bc603b9 100644
--- a/src/TopClient/Client.php
+++ b/src/TopClient/Client.php
@@ -14,12 +14,14 @@ namespace RetailCrm\TopClient;
use JMS\Serializer\SerializerInterface;
use Psr\Http\Client\ClientInterface;
+use Psr\Http\Message\StreamInterface;
use RetailCrm\Component\Exception\TopApiException;
use RetailCrm\Component\Exception\TopClientException;
use RetailCrm\Component\ServiceLocator;
use RetailCrm\Interfaces\AppDataInterface;
use RetailCrm\Interfaces\AuthenticatorInterface;
-use RetailCrm\Interfaces\RequestFactoryInterface;
+use RetailCrm\Interfaces\TopRequestFactoryInterface;
+use RetailCrm\Interfaces\TopRequestProcessorInterface;
use RetailCrm\Model\Request\BaseRequest;
use RetailCrm\Model\Response\BaseResponse;
use RetailCrm\Traits\ValidatorAwareTrait;
@@ -57,7 +59,7 @@ class Client
protected $httpClient;
/**
- * @var \RetailCrm\Interfaces\RequestFactoryInterface $requestFactory
+ * @var \RetailCrm\Interfaces\TopRequestFactoryInterface $requestFactory
* @Assert\NotNull(message="RequestFactoryInterface should be provided")
*/
protected $requestFactory;
@@ -78,6 +80,11 @@ class Client
*/
protected $timestampProvider;
+ /**
+ * @var TopRequestProcessorInterface $processor
+ */
+ protected $processor;
+
/**
* Client constructor.
*
@@ -115,9 +122,9 @@ class Client
}
/**
- * @param \RetailCrm\Interfaces\RequestFactoryInterface $requestFactory
+ * @param \RetailCrm\Interfaces\TopRequestFactoryInterface $requestFactory
*/
- public function setRequestFactory(RequestFactoryInterface $requestFactory): void
+ public function setRequestFactory(TopRequestFactoryInterface $requestFactory): void
{
$this->requestFactory = $requestFactory;
}
@@ -138,6 +145,17 @@ class Client
return $this->serviceLocator;
}
+ /**
+ * @param \RetailCrm\Interfaces\TopRequestProcessorInterface $processor
+ *
+ * @return Client
+ */
+ public function setProcessor(TopRequestProcessorInterface $processor): Client
+ {
+ $this->processor = $processor;
+ return $this;
+ }
+
/**
* @param \RetailCrm\Model\Request\BaseRequest $request
*
@@ -150,11 +168,14 @@ class Client
*/
public function sendRequest(BaseRequest $request)
{
+ $this->processor->process($request, $this->appData, $this->authenticator);
+
$httpRequest = $this->requestFactory->fromModel($request, $this->appData, $this->authenticator);
$httpResponse = $this->httpClient->sendRequest($httpRequest);
+
/** @var BaseResponse $response */
$response = $this->serializer->deserialize(
- $httpResponse->getBody()->getContents(),
+ self::getBodyContents($httpResponse->getBody()),
$request->getExpectedResponse(),
$request->format
);
@@ -169,4 +190,16 @@ class Client
return $response;
}
+
+ /**
+ * Returns body stream data (it should work like that in order to keep compatibility with some implementations).
+ *
+ * @param \Psr\Http\Message\StreamInterface $stream
+ *
+ * @return string
+ */
+ protected static function getBodyContents(StreamInterface $stream): string
+ {
+ return $stream->isSeekable() ? $stream->__toString() : $stream->getContents();
+ }
}
diff --git a/src/Traits/ValidatorAwareTrait.php b/src/Traits/ValidatorAwareTrait.php
index 8a73599..3a08747 100644
--- a/src/Traits/ValidatorAwareTrait.php
+++ b/src/Traits/ValidatorAwareTrait.php
@@ -37,10 +37,13 @@ trait ValidatorAwareTrait
/**
* @param \Symfony\Component\Validator\Validator\ValidatorInterface $validator
+ *
+ * @return $this
*/
- public function setValidator(ValidatorInterface $validator): void
+ public function setValidator(ValidatorInterface $validator): self
{
$this->validator = $validator;
+ return $this;
}
/**
diff --git a/tests/RetailCrm/Test/ClosureRequestMatcher.php b/tests/RetailCrm/Test/ClosureRequestMatcher.php
new file mode 100644
index 0000000..d7fca4c
--- /dev/null
+++ b/tests/RetailCrm/Test/ClosureRequestMatcher.php
@@ -0,0 +1,58 @@
+
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+namespace RetailCrm\Test;
+
+use Http\Message\RequestMatcher;
+use InvalidArgumentException;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Class ClosureRequestMatcher
+ *
+ * @category ClosureRequestMatcher
+ * @package RetailCrm\Test
+ * @author RetailDriver LLC
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ */
+class ClosureRequestMatcher implements RequestMatcher
+{
+ /**
+ * @var callable $matcher
+ */
+ private $matcher;
+
+ /**
+ * RequestMatcher constructor.
+ *
+ * @param callable $matcher
+ */
+ public function __construct(callable $matcher)
+ {
+ if (!is_callable($matcher)) {
+ throw new InvalidArgumentException('Matcher should be callable');
+ }
+
+ $this->matcher = $matcher;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function matches(RequestInterface $request): bool
+ {
+ $matcher = $this->matcher;
+ return $matcher($request);
+ }
+}
diff --git a/tests/RetailCrm/Test/TestCase.php b/tests/RetailCrm/Test/TestCase.php
index dabbc6f..dba8428 100644
--- a/tests/RetailCrm/Test/TestCase.php
+++ b/tests/RetailCrm/Test/TestCase.php
@@ -3,10 +3,18 @@
namespace RetailCrm\Test;
use DateTime;
+use Http\Client\Curl\Client as CurlClient;
+use Http\Discovery\Psr17FactoryDiscovery;
+use Http\Mock\Client as MockClient;
+use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Container\ContainerInterface;
+use Psr\Http\Client\ClientInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\StreamInterface;
use RetailCrm\Builder\ContainerBuilder;
use RetailCrm\Component\AppData;
use RetailCrm\Component\Authenticator\TokenAuthenticator;
+use RetailCrm\Component\Constants;
use RetailCrm\Component\Environment;
use RetailCrm\Component\Logger\StdoutLogger;
use RetailCrm\Factory\FileItemFactory;
@@ -29,17 +37,22 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
private $container;
/**
- * @param bool $recreate
+ * @param \Psr\Http\Client\ClientInterface|null $client
+ * @param bool $recreate
*
* @return \Psr\Container\ContainerInterface
*/
- protected function getContainer($recreate = false): ContainerInterface
+ protected function getContainer(?ClientInterface $client = null, $recreate = false): ContainerInterface
{
- if (null === $this->container || $recreate) {
+ if (null === $this->container || null !== $client || $recreate) {
+ $factory = new Psr17Factory();
$this->container = ContainerBuilder::create()
->setEnv(Environment::TEST)
- ->setClient(new \GuzzleHttp\Client())
+ ->setClient(is_null($client) ? self::getMockClient() : $client)
->setLogger(new StdoutLogger())
+ ->setStreamFactory($factory)
+ ->setRequestFactory($factory)
+ ->setUriFactory($factory)
->build();
}
@@ -131,6 +144,24 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
return $request;
}
+ /**
+ * @param int $code
+ * @param \RetailCrm\Model\Response\BaseResponse $response
+ *
+ * @return \Psr\Http\Message\ResponseInterface
+ */
+ protected function responseJson(int $code, $response): ResponseInterface
+ {
+ /** @var \JMS\Serializer\SerializerInterface $serializer */
+ $serializer = $this->getContainer()->get(Constants::SERIALIZER);
+ $responseFactory = Psr17FactoryDiscovery::findResponseFactory();
+ $streamFactory = Psr17FactoryDiscovery::findStreamFactory();
+
+ return $responseFactory->createResponse($code)
+ ->withHeader('Content-Type', 'application/json')
+ ->withBody($streamFactory->createStream($serializer->serialize($response, 'json')));
+ }
+
/**
* @param string $variable
* @param mixed $default
@@ -145,4 +176,39 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
return $_ENV[$variable];
}
+
+ /**
+ * @return \Http\Mock\Client
+ */
+ protected static function getMockClient(): MockClient
+ {
+ return new MockClient();
+ }
+
+ /**
+ * @return \Psr\Http\Client\ClientInterface
+ */
+ protected static function getCurlClient(): ClientInterface
+ {
+ return new CurlClient();
+ }
+
+ /**
+ * @param \Psr\Http\Message\StreamInterface $stream
+ *
+ * @return string
+ */
+ protected static function getStreamData(StreamInterface $stream): string
+ {
+ $data = '';
+
+ if ($stream->isSeekable()) {
+ $data = $stream->__toString();
+ $stream->seek(0);
+ } else {
+ $data = $stream->getContents();
+ }
+
+ return $data;
+ }
}
diff --git a/tests/RetailCrm/Tests/Builder/ContainerBuilderTest.php b/tests/RetailCrm/Tests/Builder/ContainerBuilderTest.php
new file mode 100644
index 0000000..11a33b3
--- /dev/null
+++ b/tests/RetailCrm/Tests/Builder/ContainerBuilderTest.php
@@ -0,0 +1,70 @@
+
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see http://help.retailcrm.ru
+ */
+namespace RetailCrm\Tests\Builder;
+
+use Http\Mock\Client;
+use Nyholm\Psr7\Factory\Psr17Factory;
+use Psr\Http\Message\RequestFactoryInterface;
+use Psr\Http\Message\StreamFactoryInterface;
+use Psr\Http\Message\UriFactoryInterface;
+use Psr\Log\NullLogger;
+use RetailCrm\Builder\ContainerBuilder;
+use RetailCrm\Component\Constants;
+use RetailCrm\Component\Environment;
+use RetailCrm\Component\Logger\StdoutLogger;
+use RetailCrm\Test\TestCase;
+
+/**
+ * Class ContainerBuilderTest
+ *
+ * @category ContainerBuilderTest
+ * @package RetailCrm\Tests\Builder
+ * @author RetailDriver LLC
+ * @license MIT
+ * @link http://retailcrm.ru
+ * @see https://help.retailcrm.ru
+ */
+class ContainerBuilderTest extends TestCase
+{
+ public function testBuildWithDiscovery(): void
+ {
+ $container = ContainerBuilder::create()->build();
+
+ self::assertNotNull($container->get(Constants::HTTP_CLIENT));
+ self::assertInstanceOf(NullLogger::class, $container->get(Constants::LOGGER));
+ self::assertNotNull($container->get(StreamFactoryInterface::class));
+ self::assertNotNull($container->get(RequestFactoryInterface::class));
+ self::assertNotNull($container->get(UriFactoryInterface::class));
+ }
+
+ public function testBuildWithDefinitions(): void
+ {
+ $client = new Client();
+ $logger = new StdoutLogger();
+ $factory = new Psr17Factory();
+ $container = ContainerBuilder::create()
+ ->setEnv(Environment::TEST)
+ ->setClient($client)
+ ->setLogger($logger)
+ ->setStreamFactory($factory)
+ ->setRequestFactory($factory)
+ ->setUriFactory($factory)
+ ->build();
+
+ self::assertEquals($client, $container->get(Constants::HTTP_CLIENT));
+ self::assertEquals($logger, $container->get(Constants::LOGGER));
+ self::assertEquals($factory, $container->get(StreamFactoryInterface::class));
+ self::assertEquals($factory, $container->get(RequestFactoryInterface::class));
+ self::assertEquals($factory, $container->get(UriFactoryInterface::class));
+ }
+}
diff --git a/tests/RetailCrm/Tests/Factory/RequestFactoryTest.php b/tests/RetailCrm/Tests/Factory/TopRequestFactoryTest.php
similarity index 69%
rename from tests/RetailCrm/Tests/Factory/RequestFactoryTest.php
rename to tests/RetailCrm/Tests/Factory/TopRequestFactoryTest.php
index 9296bd0..771c68c 100644
--- a/tests/RetailCrm/Tests/Factory/RequestFactoryTest.php
+++ b/tests/RetailCrm/Tests/Factory/TopRequestFactoryTest.php
@@ -3,7 +3,7 @@
/**
* PHP version 7.3
*
- * @category RequestFactoryTest
+ * @category TopRequestFactoryTest
* @package RetailCrm\Tests\Factory
* @author RetailCRM
* @license MIT
@@ -12,36 +12,34 @@
*/
namespace RetailCrm\Tests\Factory;
-use Psr\Http\Message\RequestInterface;
use RetailCrm\Component\Constants;
-use RetailCrm\Factory\RequestFactory;
-use RetailCrm\Component\AppData;
-use RetailCrm\Interfaces\RequestFactoryInterface;
+use RetailCrm\Factory\TopRequestFactory;
+use RetailCrm\Interfaces\TopRequestFactoryInterface;
use RetailCrm\Test\TestCase;
/**
- * Class RequestFactoryTest
+ * Class TopRequestFactoryTest
*
- * @category RequestFactoryTest
+ * @category TopRequestFactoryTest
* @package RetailCrm\Tests\Factory
* @author RetailDriver LLC
* @license MIT
* @link http://retailcrm.ru
* @see https://help.retailcrm.ru
*/
-class RequestFactoryTest extends TestCase
+class TopRequestFactoryTest extends TestCase
{
public function testFromModelGet(): void
{
- /** @var RequestFactory $factory */
- $factory = $this->getContainer()->get(RequestFactoryInterface::class);
+ /** @var TopRequestFactory $factory */
+ $factory = $this->getContainer()->get(TopRequestFactoryInterface::class);
$request = $factory->fromModel(
$this->getTestRequest(Constants::SIGN_TYPE_HMAC),
$this->getAppData(),
$this->getAuthenticator()
);
$uri = $request->getUri();
- $contents = $request->getBody()->getContents();
+ $contents = self::getStreamData($request->getBody());
self::assertEmpty($contents);
self::assertNotEmpty($uri->getQuery());
@@ -50,15 +48,15 @@ class RequestFactoryTest extends TestCase
public function testFromModelPost(): void
{
- /** @var RequestFactory $factory */
- $factory = $this->getContainer()->get(RequestFactoryInterface::class);
+ /** @var TopRequestFactory $factory */
+ $factory = $this->getContainer()->get(TopRequestFactoryInterface::class);
$request = $factory->fromModel(
$this->getTestRequest(Constants::SIGN_TYPE_HMAC, true, true),
$this->getAppData(),
$this->getAuthenticator()
);
$uri = $request->getUri();
- $contents = $request->getBody()->getContents();
+ $contents = self::getStreamData($request->getBody());
self::assertEmpty($uri->getQuery());
self::assertNotFalse(stripos($contents, 'The quick brown fox jumps over the lazy dog'));
diff --git a/tests/RetailCrm/Tests/TopClient/ClientTest.php b/tests/RetailCrm/Tests/TopClient/ClientTest.php
index 37de4c8..c60b5b1 100644
--- a/tests/RetailCrm/Tests/TopClient/ClientTest.php
+++ b/tests/RetailCrm/Tests/TopClient/ClientTest.php
@@ -12,10 +12,14 @@
*/
namespace RetailCrm\Tests\TopClient;
+use Psr\Http\Message\RequestInterface;
use RetailCrm\Builder\ClientBuilder;
use RetailCrm\Component\AppData;
use RetailCrm\Component\Authenticator\TokenAuthenticator;
use RetailCrm\Model\Request\HttpDnsGetRequest;
+use RetailCrm\Model\Response\BaseResponse;
+use RetailCrm\Model\Response\Body\ErrorResponseBody;
+use RetailCrm\Test\ClosureRequestMatcher;
use RetailCrm\Test\TestCase;
/**
@@ -30,16 +34,29 @@ use RetailCrm\Test\TestCase;
*/
class ClientTest extends TestCase
{
- public function testClientRequest()
+ public function testClientRequestException()
{
- self::markTestSkipped('Should be mocked!');
+ $errorBody = new ErrorResponseBody();
+ $errorBody->code = 999;
+ $errorBody->msg = 'Mocked error';
+ $errorBody->subCode = 'subcode';
+ $errorBody->requestId = '1';
+ $errorResponse = new BaseResponse();
+ $errorResponse->errorResponse = $errorBody;
+
+ $mockClient = self::getMockClient();
+ $mockClient->on(new ClosureRequestMatcher(function (RequestInterface $request) {
+ return true;
+ }), $this->responseJson(400, $errorResponse));
$client = ClientBuilder::create()
- ->setContainer($this->getContainer())
+ ->setContainer($this->getContainer($mockClient))
->setAppData(new AppData(AppData::OVERSEAS_ENDPOINT, 'appKey', 'appSecret'))
->setAuthenticator(new TokenAuthenticator('appKey', 'token'))
->build();
+ $this->expectExceptionMessage($errorBody->msg);
+
$client->sendRequest(new HttpDnsGetRequest());
}
}