1
0
mirror of synced 2024-11-25 22:36:07 +03:00

refactor (allowed to remove large portion of code), use httplug - service discovery

This commit is contained in:
Pavel 2020-09-30 13:18:35 +03:00
parent 21ff82c1b8
commit 76b5ec5cf4
22 changed files with 766 additions and 2072 deletions

View File

@ -20,23 +20,31 @@
"php": ">=7.3.0", "php": ">=7.3.0",
"ext-curl": "*", "ext-curl": "*",
"ext-json": "*", "ext-json": "*",
"psr/http-client": "^1.0", "psr/log": "^1.1",
"symfony/validator": "^5.1",
"jms/serializer": "^3.9",
"shieldon/psr-http": "^1.2",
"doctrine/annotations": "^1.10",
"doctrine/cache": "^1.10", "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": { "require-dev": {
"phpunit/phpunit": "^9.3", "phpunit/phpunit": "^9.3",
"phpmd/phpmd": "^2.9", "phpmd/phpmd": "^2.9",
"squizlabs/php_codesniffer": "^3.5", "squizlabs/php_codesniffer": "^3.5",
"guzzlehttp/guzzle": "^7.1",
"phpcompatibility/php-compatibility": "*", "phpcompatibility/php-compatibility": "*",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
"vlucas/phpdotenv": "^5.2", "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": { "scripts": {
"cghooks": "vendor/bin/cghooks", "cghooks": "vendor/bin/cghooks",

View File

@ -29,7 +29,7 @@
</rule> </rule>
<rule ref="rulesets/design.xml/CouplingBetweenObjects"> <rule ref="rulesets/design.xml/CouplingBetweenObjects">
<properties> <properties>
<property name="maximum" value="15" /> <property name="maximum" value="20" />
</properties> </properties>
</rule> </rule>
<rule ref="rulesets/naming.xml/ShortVariable"> <rule ref="rulesets/naming.xml/ShortVariable">

View File

@ -14,13 +14,14 @@ namespace RetailCrm\Builder;
use RetailCrm\Component\Constants; use RetailCrm\Component\Constants;
use RetailCrm\Component\ServiceLocator; use RetailCrm\Component\ServiceLocator;
use RetailCrm\Factory\RequestFactory; use RetailCrm\Factory\TopRequestFactory;
use RetailCrm\Interfaces\AppDataInterface; use RetailCrm\Interfaces\AppDataInterface;
use RetailCrm\Interfaces\AuthenticatorInterface; use RetailCrm\Interfaces\AuthenticatorInterface;
use RetailCrm\Interfaces\BuilderInterface; use RetailCrm\Interfaces\BuilderInterface;
use RetailCrm\Interfaces\ContainerAwareInterface; use RetailCrm\Interfaces\ContainerAwareInterface;
use RetailCrm\Interfaces\RequestFactoryInterface; use RetailCrm\Interfaces\TopRequestFactoryInterface;
use RetailCrm\Interfaces\RequestTimestampProviderInterface; use RetailCrm\Interfaces\RequestTimestampProviderInterface;
use RetailCrm\Interfaces\TopRequestProcessorInterface;
use RetailCrm\TopClient\Client; use RetailCrm\TopClient\Client;
use RetailCrm\Traits\ContainerAwareTrait; use RetailCrm\Traits\ContainerAwareTrait;
@ -84,8 +85,9 @@ class ClientBuilder implements ContainerAwareInterface, BuilderInterface
$client->setHttpClient($this->container->get(Constants::HTTP_CLIENT)); $client->setHttpClient($this->container->get(Constants::HTTP_CLIENT));
$client->setSerializer($this->container->get(Constants::SERIALIZER)); $client->setSerializer($this->container->get(Constants::SERIALIZER));
$client->setValidator($this->container->get(Constants::VALIDATOR)); $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->setServiceLocator($this->container->get(ServiceLocator::class));
$client->setProcessor($this->container->get(TopRequestProcessorInterface::class));
$client->validateSelf(); $client->validateSelf();
return $client; return $client;

View File

@ -12,8 +12,13 @@
*/ */
namespace RetailCrm\Builder; namespace RetailCrm\Builder;
use Http\Discovery\Psr17FactoryDiscovery;
use Http\Discovery\Psr18ClientDiscovery;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Psr\Http\Client\ClientInterface; 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\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use RetailCrm\Component\Constants; use RetailCrm\Component\Constants;
@ -21,18 +26,19 @@ use RetailCrm\Component\DependencyInjection\Container;
use RetailCrm\Component\Environment; use RetailCrm\Component\Environment;
use RetailCrm\Component\ServiceLocator; use RetailCrm\Component\ServiceLocator;
use RetailCrm\Factory\FileItemFactory; use RetailCrm\Factory\FileItemFactory;
use RetailCrm\Factory\RequestFactory;
use RetailCrm\Factory\SerializerFactory; use RetailCrm\Factory\SerializerFactory;
use RetailCrm\Factory\TopRequestFactory;
use RetailCrm\Interfaces\BuilderInterface; use RetailCrm\Interfaces\BuilderInterface;
use RetailCrm\Interfaces\FileItemFactoryInterface; use RetailCrm\Interfaces\FileItemFactoryInterface;
use RetailCrm\Interfaces\RequestFactoryInterface;
use RetailCrm\Interfaces\RequestSignerInterface; use RetailCrm\Interfaces\RequestSignerInterface;
use RetailCrm\Interfaces\RequestTimestampProviderInterface; use RetailCrm\Interfaces\RequestTimestampProviderInterface;
use RetailCrm\Interfaces\TopRequestFactoryInterface;
use RetailCrm\Interfaces\TopRequestProcessorInterface;
use RetailCrm\Service\RequestDataFilter; use RetailCrm\Service\RequestDataFilter;
use RetailCrm\Service\RequestSigner; use RetailCrm\Service\RequestSigner;
use RetailCrm\Service\RequestTimestampProvider; use RetailCrm\Service\RequestTimestampProvider;
use RetailCrm\Service\TopRequestProcessor;
use RuntimeException; use RuntimeException;
use Shieldon\Psr17\StreamFactory;
use Symfony\Component\Validator\Validation; use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Validator\TraceableValidator; use Symfony\Component\Validator\Validator\TraceableValidator;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
@ -55,7 +61,7 @@ class ContainerBuilder implements BuilderInterface
/** /**
* @var string $env * @var string $env
*/ */
private $env; private $env = Environment::DEV;
/** /**
* @var \Psr\Http\Client\ClientInterface $httpClient * @var \Psr\Http\Client\ClientInterface $httpClient
@ -67,6 +73,21 @@ class ContainerBuilder implements BuilderInterface
*/ */
private $logger; private $logger;
/**
* @var StreamFactoryInterface $streamFactory
*/
private $streamFactory;
/**
* @var RequestFactoryInterface $requestFactory
*/
private $requestFactory;
/**
* @var UriFactoryInterface $uriFactory
*/
private $uriFactory;
/** /**
* @return static * @return static
*/ */
@ -108,6 +129,39 @@ class ContainerBuilder implements BuilderInterface
return $this; 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 * @return \Psr\Container\ContainerInterface
*/ */
@ -136,8 +190,11 @@ class ContainerBuilder implements BuilderInterface
*/ */
protected function setProdServices(Container $container): void protected function setProdServices(Container $container): void
{ {
$container->set(Constants::HTTP_CLIENT, $this->httpClient); $container->set(Constants::HTTP_CLIENT, $this->getHttpClient());
$container->set(Constants::LOGGER, $this->logger ?: new NullLogger()); $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(RequestTimestampProviderInterface::class, new RequestTimestampProvider());
$container->set( $container->set(
Constants::VALIDATOR, Constants::VALIDATOR,
@ -146,7 +203,9 @@ class ContainerBuilder implements BuilderInterface
$container->set(Constants::SERIALIZER, function (ContainerInterface $container) { $container->set(Constants::SERIALIZER, function (ContainerInterface $container) {
return SerializerFactory::withContainer($container)->create(); 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(RequestDataFilter::class, new RequestDataFilter());
$container->set(RequestSignerInterface::class, function (ContainerInterface $container) { $container->set(RequestSignerInterface::class, function (ContainerInterface $container) {
return new RequestSigner( return new RequestSigner(
@ -154,14 +213,19 @@ class ContainerBuilder implements BuilderInterface
$container->get(RequestDataFilter::class) $container->get(RequestDataFilter::class)
); );
}); });
$container->set(RequestFactoryInterface::class, function (ContainerInterface $container) { $container->set(TopRequestProcessorInterface::class, function (ContainerInterface $container) {
return new RequestFactory( return (new TopRequestProcessor())
$container->get(RequestSignerInterface::class), ->setSigner($container->get(RequestSignerInterface::class))
$container->get(RequestDataFilter::class), ->setValidator($container->get(Constants::VALIDATOR))
$container->get(Constants::SERIALIZER), ->setTimestampProvider($container->get(RequestTimestampProviderInterface::class));
$container->get(Constants::VALIDATOR), });
$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) { $container->set(ServiceLocator::class, function (ContainerInterface $container) {
$locator = new ServiceLocator(); $locator = new ServiceLocator();
@ -182,4 +246,50 @@ class ContainerBuilder implements BuilderInterface
$container->set('validator', new TraceableValidator($validator)); $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();
}
} }

View File

@ -1,312 +0,0 @@
<?php
/**
* PHP version 7.3
*
* @category AppendStream
* @package RetailCrm\Component\Psr7
* @author RetailCRM <integration@retailcrm.ru>
* @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 <mtdowling@gmail.com>
* @author RetailDriver LLC <integration@retailcrm.ru>
* @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 : [];
}
}

View File

@ -1,198 +0,0 @@
<?php
/**
* PHP version 7.3
*
* @category BufferStream
* @package RetailCrm\Component\Psr7
* @author RetailCRM <integration@retailcrm.ru>
* @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 <mtdowling@gmail.com>
* @author RetailDriver LLC <integration@retailcrm.ru>
* @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 : [];
}
}

View File

@ -1,391 +0,0 @@
<?php
/**
* PHP version 7.3
*
* @category MultipartStream
* @package RetailCrm\Component\Psr7
* @author RetailCRM <integration@retailcrm.ru>
* @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 <mtdowling@gmail.com>
* @author RetailDriver LLC <integration@retailcrm.ru>
* @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<string, string> $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;
}
}

View File

@ -1,242 +0,0 @@
<?php
/**
* PHP version 7.3
*
* @category PumpStream
* @package RetailCrm\Component\Psr7
* @author RetailCRM <integration@retailcrm.ru>
* @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 <mtdowling@gmail.com>
* @author RetailDriver LLC <integration@retailcrm.ru>
* @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);
}
}
}

View File

@ -1,375 +0,0 @@
<?php
/**
* PHP version 7.3
*
* @category Stream
* @package RetailCrm\Component\Psr7
* @author RetailCRM <integration@retailcrm.ru>
* @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 <mtdowling@gmail.com>
* @author RetailDriver LLC <integration@retailcrm.ru>
* @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;
}
}

View File

@ -1,308 +0,0 @@
<?php
/**
* PHP version 7.3
*
* @category Utils
* @package RetailCrm\Component\Psr7
* @author RetailCRM <integration@retailcrm.ru>
* @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 <mtdowling@gmail.com>
* @author RetailDriver LLC <integration@retailcrm.ru>
* @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;
}
}

View File

@ -1,188 +0,0 @@
<?php
/**
* PHP version 7.3
*
* @category RequestFactory
* @package RetailCrm\Factory
* @author RetailCRM <integration@retailcrm.ru>
* @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 <integration@retailcrm.ru>
* @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',
];
}
}

View File

@ -0,0 +1,214 @@
<?php
/**
* PHP version 7.3
*
* @category TopRequestFactory
* @package RetailCrm\Factory
* @author RetailCRM <integration@retailcrm.ru>
* @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 <integration@retailcrm.ru>
* @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));
}
}
}

View File

@ -3,7 +3,7 @@
/** /**
* PHP version 7.3 * PHP version 7.3
* *
* @category RequestFactoryInterface * @category TopRequestFactoryInterface
* @package RetailCrm\Interfaces * @package RetailCrm\Interfaces
* @author RetailCRM <integration@retailcrm.ru> * @author RetailCRM <integration@retailcrm.ru>
* @license MIT https://mit-license.org * @license MIT https://mit-license.org
@ -17,16 +17,16 @@ use Psr\Http\Message\RequestInterface;
use RetailCrm\Model\Request\BaseRequest; use RetailCrm\Model\Request\BaseRequest;
/** /**
* Interface RequestFactoryInterface * Interface TopRequestFactoryInterface
* *
* @category RequestFactoryInterface * @category TopRequestFactoryInterface
* @package RetailCrm\Interfaces * @package RetailCrm\Interfaces
* @author RetailDriver LLC <integration@retailcrm.ru> * @author RetailDriver LLC <integration@retailcrm.ru>
* @license MIT https://mit-license.org * @license MIT https://mit-license.org
* @link http://retailcrm.ru * @link http://retailcrm.ru
* @see https://help.retailcrm.ru * @see https://help.retailcrm.ru
*/ */
interface RequestFactoryInterface interface TopRequestFactoryInterface
{ {
/** /**
* @param \RetailCrm\Model\Request\BaseRequest $request * @param \RetailCrm\Model\Request\BaseRequest $request

View File

@ -0,0 +1,47 @@
<?php
/**
* PHP version 7.3
*
* @category TopRequestProcessorInterface
* @package RetailCrm\Interfaces
* @author RetailCRM <integration@retailcrm.ru>
* @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 <integration@retailcrm.ru>
* @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;
}

View File

@ -0,0 +1,82 @@
<?php
/**
* PHP version 7.3
*
* @category TopRequestProcessor
* @package RetailCrm\Service
* @author RetailCRM <integration@retailcrm.ru>
* @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 <integration@retailcrm.ru>
* @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);
}
}

View File

@ -14,12 +14,14 @@ namespace RetailCrm\TopClient;
use JMS\Serializer\SerializerInterface; use JMS\Serializer\SerializerInterface;
use Psr\Http\Client\ClientInterface; use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\StreamInterface;
use RetailCrm\Component\Exception\TopApiException; use RetailCrm\Component\Exception\TopApiException;
use RetailCrm\Component\Exception\TopClientException; use RetailCrm\Component\Exception\TopClientException;
use RetailCrm\Component\ServiceLocator; use RetailCrm\Component\ServiceLocator;
use RetailCrm\Interfaces\AppDataInterface; use RetailCrm\Interfaces\AppDataInterface;
use RetailCrm\Interfaces\AuthenticatorInterface; use RetailCrm\Interfaces\AuthenticatorInterface;
use RetailCrm\Interfaces\RequestFactoryInterface; use RetailCrm\Interfaces\TopRequestFactoryInterface;
use RetailCrm\Interfaces\TopRequestProcessorInterface;
use RetailCrm\Model\Request\BaseRequest; use RetailCrm\Model\Request\BaseRequest;
use RetailCrm\Model\Response\BaseResponse; use RetailCrm\Model\Response\BaseResponse;
use RetailCrm\Traits\ValidatorAwareTrait; use RetailCrm\Traits\ValidatorAwareTrait;
@ -57,7 +59,7 @@ class Client
protected $httpClient; protected $httpClient;
/** /**
* @var \RetailCrm\Interfaces\RequestFactoryInterface $requestFactory * @var \RetailCrm\Interfaces\TopRequestFactoryInterface $requestFactory
* @Assert\NotNull(message="RequestFactoryInterface should be provided") * @Assert\NotNull(message="RequestFactoryInterface should be provided")
*/ */
protected $requestFactory; protected $requestFactory;
@ -78,6 +80,11 @@ class Client
*/ */
protected $timestampProvider; protected $timestampProvider;
/**
* @var TopRequestProcessorInterface $processor
*/
protected $processor;
/** /**
* Client constructor. * 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; $this->requestFactory = $requestFactory;
} }
@ -138,6 +145,17 @@ class Client
return $this->serviceLocator; 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 * @param \RetailCrm\Model\Request\BaseRequest $request
* *
@ -150,11 +168,14 @@ class Client
*/ */
public function sendRequest(BaseRequest $request) public function sendRequest(BaseRequest $request)
{ {
$this->processor->process($request, $this->appData, $this->authenticator);
$httpRequest = $this->requestFactory->fromModel($request, $this->appData, $this->authenticator); $httpRequest = $this->requestFactory->fromModel($request, $this->appData, $this->authenticator);
$httpResponse = $this->httpClient->sendRequest($httpRequest); $httpResponse = $this->httpClient->sendRequest($httpRequest);
/** @var BaseResponse $response */ /** @var BaseResponse $response */
$response = $this->serializer->deserialize( $response = $this->serializer->deserialize(
$httpResponse->getBody()->getContents(), self::getBodyContents($httpResponse->getBody()),
$request->getExpectedResponse(), $request->getExpectedResponse(),
$request->format $request->format
); );
@ -169,4 +190,16 @@ class Client
return $response; 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();
}
} }

View File

@ -37,10 +37,13 @@ trait ValidatorAwareTrait
/** /**
* @param \Symfony\Component\Validator\Validator\ValidatorInterface $validator * @param \Symfony\Component\Validator\Validator\ValidatorInterface $validator
*
* @return $this
*/ */
public function setValidator(ValidatorInterface $validator): void public function setValidator(ValidatorInterface $validator): self
{ {
$this->validator = $validator; $this->validator = $validator;
return $this;
} }
/** /**

View File

@ -0,0 +1,58 @@
<?php
/**
* PHP version 7.3
*
* @category RequestMatcher
* @package RetailCrm\Test
* @author RetailCRM <integration@retailcrm.ru>
* @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 <integration@retailcrm.ru>
* @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);
}
}

View File

@ -3,10 +3,18 @@
namespace RetailCrm\Test; namespace RetailCrm\Test;
use DateTime; 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\Container\ContainerInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use RetailCrm\Builder\ContainerBuilder; use RetailCrm\Builder\ContainerBuilder;
use RetailCrm\Component\AppData; use RetailCrm\Component\AppData;
use RetailCrm\Component\Authenticator\TokenAuthenticator; use RetailCrm\Component\Authenticator\TokenAuthenticator;
use RetailCrm\Component\Constants;
use RetailCrm\Component\Environment; use RetailCrm\Component\Environment;
use RetailCrm\Component\Logger\StdoutLogger; use RetailCrm\Component\Logger\StdoutLogger;
use RetailCrm\Factory\FileItemFactory; use RetailCrm\Factory\FileItemFactory;
@ -29,17 +37,22 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
private $container; private $container;
/** /**
* @param \Psr\Http\Client\ClientInterface|null $client
* @param bool $recreate * @param bool $recreate
* *
* @return \Psr\Container\ContainerInterface * @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() $this->container = ContainerBuilder::create()
->setEnv(Environment::TEST) ->setEnv(Environment::TEST)
->setClient(new \GuzzleHttp\Client()) ->setClient(is_null($client) ? self::getMockClient() : $client)
->setLogger(new StdoutLogger()) ->setLogger(new StdoutLogger())
->setStreamFactory($factory)
->setRequestFactory($factory)
->setUriFactory($factory)
->build(); ->build();
} }
@ -131,6 +144,24 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
return $request; 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 string $variable
* @param mixed $default * @param mixed $default
@ -145,4 +176,39 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
return $_ENV[$variable]; 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;
}
} }

View File

@ -0,0 +1,70 @@
<?php
/**
* PHP version 7.3
*
* @category ContainerBuilderTest
* @package RetailCrm\Tests\Builder
* @author RetailCRM <integration@retailcrm.ru>
* @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 <integration@retailcrm.ru>
* @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));
}
}

View File

@ -3,7 +3,7 @@
/** /**
* PHP version 7.3 * PHP version 7.3
* *
* @category RequestFactoryTest * @category TopRequestFactoryTest
* @package RetailCrm\Tests\Factory * @package RetailCrm\Tests\Factory
* @author RetailCRM <integration@retailcrm.ru> * @author RetailCRM <integration@retailcrm.ru>
* @license MIT * @license MIT
@ -12,36 +12,34 @@
*/ */
namespace RetailCrm\Tests\Factory; namespace RetailCrm\Tests\Factory;
use Psr\Http\Message\RequestInterface;
use RetailCrm\Component\Constants; use RetailCrm\Component\Constants;
use RetailCrm\Factory\RequestFactory; use RetailCrm\Factory\TopRequestFactory;
use RetailCrm\Component\AppData; use RetailCrm\Interfaces\TopRequestFactoryInterface;
use RetailCrm\Interfaces\RequestFactoryInterface;
use RetailCrm\Test\TestCase; use RetailCrm\Test\TestCase;
/** /**
* Class RequestFactoryTest * Class TopRequestFactoryTest
* *
* @category RequestFactoryTest * @category TopRequestFactoryTest
* @package RetailCrm\Tests\Factory * @package RetailCrm\Tests\Factory
* @author RetailDriver LLC <integration@retailcrm.ru> * @author RetailDriver LLC <integration@retailcrm.ru>
* @license MIT * @license MIT
* @link http://retailcrm.ru * @link http://retailcrm.ru
* @see https://help.retailcrm.ru * @see https://help.retailcrm.ru
*/ */
class RequestFactoryTest extends TestCase class TopRequestFactoryTest extends TestCase
{ {
public function testFromModelGet(): void public function testFromModelGet(): void
{ {
/** @var RequestFactory $factory */ /** @var TopRequestFactory $factory */
$factory = $this->getContainer()->get(RequestFactoryInterface::class); $factory = $this->getContainer()->get(TopRequestFactoryInterface::class);
$request = $factory->fromModel( $request = $factory->fromModel(
$this->getTestRequest(Constants::SIGN_TYPE_HMAC), $this->getTestRequest(Constants::SIGN_TYPE_HMAC),
$this->getAppData(), $this->getAppData(),
$this->getAuthenticator() $this->getAuthenticator()
); );
$uri = $request->getUri(); $uri = $request->getUri();
$contents = $request->getBody()->getContents(); $contents = self::getStreamData($request->getBody());
self::assertEmpty($contents); self::assertEmpty($contents);
self::assertNotEmpty($uri->getQuery()); self::assertNotEmpty($uri->getQuery());
@ -50,15 +48,15 @@ class RequestFactoryTest extends TestCase
public function testFromModelPost(): void public function testFromModelPost(): void
{ {
/** @var RequestFactory $factory */ /** @var TopRequestFactory $factory */
$factory = $this->getContainer()->get(RequestFactoryInterface::class); $factory = $this->getContainer()->get(TopRequestFactoryInterface::class);
$request = $factory->fromModel( $request = $factory->fromModel(
$this->getTestRequest(Constants::SIGN_TYPE_HMAC, true, true), $this->getTestRequest(Constants::SIGN_TYPE_HMAC, true, true),
$this->getAppData(), $this->getAppData(),
$this->getAuthenticator() $this->getAuthenticator()
); );
$uri = $request->getUri(); $uri = $request->getUri();
$contents = $request->getBody()->getContents(); $contents = self::getStreamData($request->getBody());
self::assertEmpty($uri->getQuery()); self::assertEmpty($uri->getQuery());
self::assertNotFalse(stripos($contents, 'The quick brown fox jumps over the lazy dog')); self::assertNotFalse(stripos($contents, 'The quick brown fox jumps over the lazy dog'));

View File

@ -12,10 +12,14 @@
*/ */
namespace RetailCrm\Tests\TopClient; namespace RetailCrm\Tests\TopClient;
use Psr\Http\Message\RequestInterface;
use RetailCrm\Builder\ClientBuilder; use RetailCrm\Builder\ClientBuilder;
use RetailCrm\Component\AppData; use RetailCrm\Component\AppData;
use RetailCrm\Component\Authenticator\TokenAuthenticator; use RetailCrm\Component\Authenticator\TokenAuthenticator;
use RetailCrm\Model\Request\HttpDnsGetRequest; use RetailCrm\Model\Request\HttpDnsGetRequest;
use RetailCrm\Model\Response\BaseResponse;
use RetailCrm\Model\Response\Body\ErrorResponseBody;
use RetailCrm\Test\ClosureRequestMatcher;
use RetailCrm\Test\TestCase; use RetailCrm\Test\TestCase;
/** /**
@ -30,16 +34,29 @@ use RetailCrm\Test\TestCase;
*/ */
class ClientTest extends 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() $client = ClientBuilder::create()
->setContainer($this->getContainer()) ->setContainer($this->getContainer($mockClient))
->setAppData(new AppData(AppData::OVERSEAS_ENDPOINT, 'appKey', 'appSecret')) ->setAppData(new AppData(AppData::OVERSEAS_ENDPOINT, 'appKey', 'appSecret'))
->setAuthenticator(new TokenAuthenticator('appKey', 'token')) ->setAuthenticator(new TokenAuthenticator('appKey', 'token'))
->build(); ->build();
$this->expectExceptionMessage($errorBody->msg);
$client->sendRequest(new HttpDnsGetRequest()); $client->sendRequest(new HttpDnsGetRequest());
} }
} }