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()); } }