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