From a6aec8e7375e391a8c639346e19bc88d03556f02 Mon Sep 17 00:00:00 2001 From: zeljko Date: Mon, 8 Apr 2024 15:14:25 +0200 Subject: [PATCH 1/3] Minor fixes --- composer.lock | 8 +- functions.php | 14 +++ src/Command/DummyCommand.php | 117 +++++++++++++++++++- src/DTO/Zoho/Item.php | 10 +- src/DTO/Zoho/Items.php | 3 +- src/Entity/Product/Product.php | 3 + src/Form/DataComparator/MoneyComparator.php | 2 - src/Service/Zoho/Sync/ProductSync.php | 2 +- 8 files changed, 142 insertions(+), 17 deletions(-) diff --git a/composer.lock b/composer.lock index b99068b..602c015 100644 --- a/composer.lock +++ b/composer.lock @@ -2762,12 +2762,12 @@ "source": { "type": "git", "url": "https://github.com/strictify/form-mapper-bundle.git", - "reference": "939818b29cafffcba94d93334b56859e0f45eeab" + "reference": "485ef9a26dc50f20fc4ff3f9e470cf742015bb35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/strictify/form-mapper-bundle/zipball/939818b29cafffcba94d93334b56859e0f45eeab", - "reference": "939818b29cafffcba94d93334b56859e0f45eeab", + "url": "https://api.github.com/repos/strictify/form-mapper-bundle/zipball/485ef9a26dc50f20fc4ff3f9e470cf742015bb35", + "reference": "485ef9a26dc50f20fc4ff3f9e470cf742015bb35", "shasum": "" }, "require": { @@ -2827,7 +2827,7 @@ "source": "https://github.com/strictify/form-mapper-bundle/tree/master", "issues": "https://github.com/strictify/form-mapper-bundle/issues" }, - "time": "2024-03-11T16:57:31+00:00" + "time": "2024-04-08T10:20:01+00:00" }, { "name": "strictify/lazy", diff --git a/functions.php b/functions.php index 438bf51..b3d11d1 100644 --- a/functions.php +++ b/functions.php @@ -162,3 +162,17 @@ function implode_non_empty(string $separator, array $parts): ?string return implode($separator, $filtered) ?: null; } + +/** + * @param non-empty-list $lookup + */ +function any_key_exists(array $lookup, array $haystack): bool +{ + foreach ($lookup as $item) { + if (array_key_exists($item, $haystack)) { + return true; + } + } + + return false; +} diff --git a/src/Command/DummyCommand.php b/src/Command/DummyCommand.php index 12c7e16..772b6e0 100644 --- a/src/Command/DummyCommand.php +++ b/src/Command/DummyCommand.php @@ -4,13 +4,20 @@ namespace App\Command; -use LogicException; +use App\DTO\Zoho\Items; use App\Service\Zoho\ZohoManager; +use CuyZ\Valinor\Mapper\TreeMapper; +use CuyZ\Valinor\Mapper\MappingError; +use CuyZ\Valinor\Mapper\Source\Source; use App\Repository\Company\CompanyRepository; use Symfony\Component\Console\Command\Command; +use CuyZ\Valinor\Mapper\Tree\Message\Messages; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use CuyZ\ValinorBundle\Configurator\Attributes\SupportDateFormats; +use CuyZ\ValinorBundle\Configurator\Attributes\AllowSuperfluousKeys; +use function json_decode; /** * @psalm-suppress all @@ -22,6 +29,8 @@ class DummyCommand extends Command { public function __construct( + #[SupportDateFormats('Y-m-d', 'Y-m-d H:i'), AllowSuperfluousKeys] + private TreeMapper $treeMapper, private ZohoManager $zohoSync, private CompanyRepository $companyRepository, ) @@ -31,9 +40,109 @@ public function __construct( protected function execute(InputInterface $input, OutputInterface $output): int { - $company = $this->companyRepository->findOneBy(['name' => 'Strictify']) ?? throw new LogicException(); - $this->zohoSync->downloadAll($company); + try { + $data = $this->json(); + $vars = json_decode($data, true); +// dump($vars); + $dto = $this->treeMapper->map(Items::class, Source::array($vars)->camelCaseKeys()); + + dump($dto); +// +// $company = $this->companyRepository->findOneBy(['name' => 'Strictify']) ?? throw new LogicException(); +// $this->zohoSync->downloadAll($company); + + return Command::SUCCESS; + } catch (MappingError $error) { + dump($error->getMessage()); + $messages = Messages::flattenFromNode( + $error->node() + ); + dump($messages); + +// foreach ($messages as $message) { +// if ($message->code() === 'some_code') { +// $message = $message +// ->withParameter('some_parameter', 'some custom value') +// ->withBody('new message / {message_code} / {some_parameter}'); +// } +// +// // new message / some_code / some custom value +// echo $message; +// } + } + + return 0; + } + + private function json(): string + { + return <<itemId; } @@ -39,9 +39,9 @@ public function getRate(): ?float } /** - * @return non-empty-string|null + * @return non-empty-string|int|null */ - public function getTaxId(): ?string + public function getTaxId(): string|int|null { $taxId = $this->taxId; if (is_string($taxId) && $taxId !== '') { diff --git a/src/DTO/Zoho/Items.php b/src/DTO/Zoho/Items.php index b860311..efe1707 100644 --- a/src/DTO/Zoho/Items.php +++ b/src/DTO/Zoho/Items.php @@ -12,7 +12,8 @@ class Items public function __construct( public int $code, public string $message, - public array $items, + public ?Item $item = null, + public array $items = [], ) { } diff --git a/src/Entity/Product/Product.php b/src/Entity/Product/Product.php index dfab763..c509494 100644 --- a/src/Entity/Product/Product.php +++ b/src/Entity/Product/Product.php @@ -77,6 +77,9 @@ public function setDescription(?string $description): void } #[Override] + /** + * @return non-empty-string|null + */ public function getZohoId(): ?string { $zohoId = $this->zohoId; diff --git a/src/Form/DataComparator/MoneyComparator.php b/src/Form/DataComparator/MoneyComparator.php index 042435c..db6a96c 100644 --- a/src/Form/DataComparator/MoneyComparator.php +++ b/src/Form/DataComparator/MoneyComparator.php @@ -7,9 +7,7 @@ use Override; use Money\Money; use Strictify\FormMapper\Service\Comparator\DataComparatorInterface; -use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; -#[AutoconfigureTag('strictify_form_mapper.comparator')] class MoneyComparator implements DataComparatorInterface { #[Override] diff --git a/src/Service/Zoho/Sync/ProductSync.php b/src/Service/Zoho/Sync/ProductSync.php index 5776b50..2927e26 100644 --- a/src/Service/Zoho/Sync/ProductSync.php +++ b/src/Service/Zoho/Sync/ProductSync.php @@ -107,7 +107,7 @@ private function getProduct(ZohoItem $zohoItem, Company $company): Product if ($product = $this->productRepository->findOneBy(['zohoId' => $zohoItem->getItemId()])) { return $product; } - $product = new Product($company, name: $zohoItem->getName(), zohoId: $zohoItem->getItemId()); + $product = new Product($company, name: $zohoItem->getName(), zohoId: (string)$zohoItem->getItemId()); $this->productRepository->persist($product); return $product; From 9e9ca725d4a3741459c5033e152e32758fca2965 Mon Sep 17 00:00:00 2001 From: zeljko Date: Mon, 8 Apr 2024 20:44:42 +0200 Subject: [PATCH 2/3] Improved sync --- src/Command/DummyCommand.php | 122 ++---------------- src/DTO/Zoho/Item.php | 15 ++- src/DTO/Zoho/Items.php | 17 ++- src/DTO/Zoho/Tax.php | 20 ++- src/DTO/Zoho/Taxes.php | 20 ++- src/DTO/Zoho/Warehouse.php | 17 ++- src/DTO/Zoho/Warehouses.php | 20 ++- src/DTO/Zoho/ZohoMappingInterface.php | 21 +++ src/DTO/Zoho/ZohoSingleResultInterface.php | 13 ++ src/Entity/IdTrait.php | 3 + src/Entity/ZohoAwareInterface.php | 9 ++ src/Form/Entity/Product/ProductType.php | 1 + src/Message/Zoho/ZohoPutEntityMessage.php | 60 +++++++++ src/Service/Zoho/Model/SyncInterface.php | 66 ++++++++++ src/Service/Zoho/Sync/Model/SyncInterface.php | 35 ----- src/Service/Zoho/Sync/ProductSync.php | 109 +++++++--------- src/Service/Zoho/Sync/TaxSync.php | 83 +++++------- src/Service/Zoho/Sync/WarehouseSync.php | 77 +++++------ src/Service/Zoho/ZohoClient.php | 66 +++++----- ...ohoManager.php => ZohoImprovedManager.php} | 73 ++++++++--- 20 files changed, 472 insertions(+), 375 deletions(-) create mode 100644 src/DTO/Zoho/ZohoMappingInterface.php create mode 100644 src/DTO/Zoho/ZohoSingleResultInterface.php create mode 100644 src/Message/Zoho/ZohoPutEntityMessage.php create mode 100644 src/Service/Zoho/Model/SyncInterface.php delete mode 100644 src/Service/Zoho/Sync/Model/SyncInterface.php rename src/Service/{Zoho/ZohoManager.php => ZohoImprovedManager.php} (61%) diff --git a/src/Command/DummyCommand.php b/src/Command/DummyCommand.php index 772b6e0..70e620f 100644 --- a/src/Command/DummyCommand.php +++ b/src/Command/DummyCommand.php @@ -1,23 +1,18 @@ -json(); - $vars = json_decode($data, true); -// dump($vars); - $dto = $this->treeMapper->map(Items::class, Source::array($vars)->camelCaseKeys()); - - dump($dto); -// -// $company = $this->companyRepository->findOneBy(['name' => 'Strictify']) ?? throw new LogicException(); -// $this->zohoSync->downloadAll($company); - - return Command::SUCCESS; - } catch (MappingError $error) { - dump($error->getMessage()); - $messages = Messages::flattenFromNode( - $error->node() - ); - dump($messages); - -// foreach ($messages as $message) { -// if ($message->code() === 'some_code') { -// $message = $message -// ->withParameter('some_parameter', 'some custom value') -// ->withBody('new message / {message_code} / {some_parameter}'); -// } -// -// // new message / some_code / some custom value -// echo $message; -// } - } - - return 0; - } - - private function json(): string - { - return <<companyRepository->findOneBy(['name' => 'Strictify']) ?? throw new LogicException(); + $this->zohoImprovedManager->downloadAll($company); + return Command::SUCCESS; } } diff --git a/src/DTO/Zoho/Item.php b/src/DTO/Zoho/Item.php index 24a344c..cac7f99 100644 --- a/src/DTO/Zoho/Item.php +++ b/src/DTO/Zoho/Item.php @@ -4,10 +4,14 @@ namespace App\DTO\Zoho; +use Override; use function is_string; -class Item +class Item implements ZohoSingleResultInterface { + /** + * @param non-empty-string|int $itemId + */ public function __construct( private string|int $itemId, private string $name, @@ -18,6 +22,9 @@ public function __construct( { } + /** + * @return non-empty-string|int + */ public function getItemId(): string|int { return $this->itemId; @@ -50,4 +57,10 @@ public function getTaxId(): string|int|null return null; } + + #[Override] + public function getId(): string + { + return (string)$this->getItemId(); + } } diff --git a/src/DTO/Zoho/Items.php b/src/DTO/Zoho/Items.php index efe1707..def6ed7 100644 --- a/src/DTO/Zoho/Items.php +++ b/src/DTO/Zoho/Items.php @@ -4,7 +4,12 @@ namespace App\DTO\Zoho; -class Items +use LogicException; + +/** + * @implements ZohoMappingInterface + */ +class Items implements ZohoMappingInterface { /** * @param list $items @@ -17,4 +22,14 @@ public function __construct( ) { } + + public function getOne(): object + { + return $this->item ?? throw new LogicException(); + } + + public function getMany(): array + { + return $this->items; + } } diff --git a/src/DTO/Zoho/Tax.php b/src/DTO/Zoho/Tax.php index 486a5d8..8592aae 100644 --- a/src/DTO/Zoho/Tax.php +++ b/src/DTO/Zoho/Tax.php @@ -4,17 +4,25 @@ namespace App\DTO\Zoho; -class Tax +use Override; + +class Tax implements ZohoSingleResultInterface { + /** + * @param non-empty-string|int $taxId + */ public function __construct( - private string $taxId, + private string|int $taxId, private string $taxName, private float $taxPercentage, ) { } - public function getTaxId(): string + /** + * @return non-empty-string|int + */ + public function getTaxId(): string|int { return $this->taxId; } @@ -28,4 +36,10 @@ public function getTaxPercentage(): float { return $this->taxPercentage; } + + #[Override] + public function getId(): string + { + return (string)$this->getTaxId(); + } } diff --git a/src/DTO/Zoho/Taxes.php b/src/DTO/Zoho/Taxes.php index 6274491..c7bb7a6 100644 --- a/src/DTO/Zoho/Taxes.php +++ b/src/DTO/Zoho/Taxes.php @@ -4,7 +4,12 @@ namespace App\DTO\Zoho; -class Taxes +use LogicException; + +/** + * @implements ZohoMappingInterface + */ +class Taxes implements ZohoMappingInterface { /** * @param list $taxes @@ -12,8 +17,19 @@ class Taxes public function __construct( public int $code, public string $message, - public array $taxes, + private array $taxes, + private ?Tax $tax = null, ) { } + + public function getOne(): object + { + return $this->tax ?? throw new LogicException(); + } + + public function getMany(): array + { + return $this->taxes; + } } diff --git a/src/DTO/Zoho/Warehouse.php b/src/DTO/Zoho/Warehouse.php index b6cb17b..e05995d 100644 --- a/src/DTO/Zoho/Warehouse.php +++ b/src/DTO/Zoho/Warehouse.php @@ -4,22 +4,33 @@ namespace App\DTO\Zoho; -class Warehouse +class Warehouse implements ZohoSingleResultInterface { + /** + * @param non-empty-string|int $warehouseId + */ public function __construct( - private string $warehouseId, + private string|int $warehouseId, private string $warehouseName, ) { } + /** + * @return non-empty-string + */ public function getWarehouseId(): string { - return $this->warehouseId; + return (string)$this->warehouseId; } public function getName(): string { return $this->warehouseName; } + + public function getId(): string + { + return (string)$this->warehouseId; + } } diff --git a/src/DTO/Zoho/Warehouses.php b/src/DTO/Zoho/Warehouses.php index 4273abf..4ba5a06 100644 --- a/src/DTO/Zoho/Warehouses.php +++ b/src/DTO/Zoho/Warehouses.php @@ -4,7 +4,12 @@ namespace App\DTO\Zoho; -class Warehouses +use LogicException; + +/** + * @implements ZohoMappingInterface + */ +class Warehouses implements ZohoMappingInterface { /** * @param list $warehouses @@ -12,8 +17,19 @@ class Warehouses public function __construct( public int $code, public string $message, - public array $warehouses, + private ?Warehouse $warehouse = null, + public array $warehouses = [], ) { } + + public function getOne(): object + { + return $this->warehouse ?? throw new LogicException(); + } + + public function getMany(): array + { + return $this->warehouses; + } } diff --git a/src/DTO/Zoho/ZohoMappingInterface.php b/src/DTO/Zoho/ZohoMappingInterface.php new file mode 100644 index 0000000..044409d --- /dev/null +++ b/src/DTO/Zoho/ZohoMappingInterface.php @@ -0,0 +1,21 @@ + + */ + public function getMany(): array; +} diff --git a/src/DTO/Zoho/ZohoSingleResultInterface.php b/src/DTO/Zoho/ZohoSingleResultInterface.php new file mode 100644 index 0000000..f29ded3 --- /dev/null +++ b/src/DTO/Zoho/ZohoSingleResultInterface.php @@ -0,0 +1,13 @@ +id ??= $this->doCreateUuid(); diff --git a/src/Entity/ZohoAwareInterface.php b/src/Entity/ZohoAwareInterface.php index f875bed..f07799b 100644 --- a/src/Entity/ZohoAwareInterface.php +++ b/src/Entity/ZohoAwareInterface.php @@ -4,10 +4,19 @@ namespace App\Entity; +use App\Entity\Company\Company; + interface ZohoAwareInterface { + public function getCompany(): Company; + /** * @return non-empty-string|null */ public function getZohoId(): ?string; + + /** + * @return non-empty-string + */ + public function getId(): string; } diff --git a/src/Form/Entity/Product/ProductType.php b/src/Form/Entity/Product/ProductType.php index f16bb96..88f19ff 100644 --- a/src/Form/Entity/Product/ProductType.php +++ b/src/Form/Entity/Product/ProductType.php @@ -54,6 +54,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('tax', EntityType::class, [ 'class' => Tax::class, + 'disabled' => true, 'required' => false, 'placeholder' => '--', 'get_value' => fn(Product $product) => $product->getTax(), diff --git a/src/Message/Zoho/ZohoPutEntityMessage.php b/src/Message/Zoho/ZohoPutEntityMessage.php new file mode 100644 index 0000000..fa8d1e0 --- /dev/null +++ b/src/Message/Zoho/ZohoPutEntityMessage.php @@ -0,0 +1,60 @@ + + */ + private string $class; + + /** + * @var non-empty-string + */ + private string $zohoId; + + /** + * @param TAction $action + */ + public function __construct(ZohoAwareInterface $entity, #[ExpectedValues(['put', 'delete'])] private string $action = 'put') + { + $this->class = get_class($entity); + $this->zohoId = $entity->getZohoId() ?? throw new LogicException(); + } + + /** + * @return class-string + */ + public function getClass(): string + { + return $this->class; + } + + /** + * @return non-empty-string + */ + public function getZohoId(): string + { + return $this->zohoId; + } + + /** + * @return TAction + */ + public function getAction(): string + { + return $this->action; + } +} diff --git a/src/Service/Zoho/Model/SyncInterface.php b/src/Service/Zoho/Model/SyncInterface.php new file mode 100644 index 0000000..24e6bdb --- /dev/null +++ b/src/Service/Zoho/Model/SyncInterface.php @@ -0,0 +1,66 @@ + + */ +#[AutoconfigureTag(name: self::class)] +interface SyncInterface +{ + /** + * @param non-empty-string $zohoId + * + * @return TEntity|null + */ + public function findEntityByZohoId(string $zohoId): ZohoAwareInterface|null; + + /** + * @param TEntity $entity + * + * @return non-empty-array + */ + public function createPutPayload(ZohoAwareInterface $entity): array; + + /** + * @return class-string + */ + public function getEntityClass(): string; + + /** + * @return class-string + */ + public function getMappingClass(): string; + + /** + * @return non-empty-string + */ + public function getBaseUrl(): string; + + /** + * When an entity from @see getEntityName is updated, return iterable of messages to be executed after succesful flush. + * + * @no-named-arguments + * + * @param TEntity $entity + * + * @return iterable + */ + public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable; + + /** + * @param TEntity $entity + * @param TK $mapping + */ + public function map(ZohoAwareInterface $entity, object $mapping): void; +} diff --git a/src/Service/Zoho/Sync/Model/SyncInterface.php b/src/Service/Zoho/Sync/Model/SyncInterface.php deleted file mode 100644 index 8613de0..0000000 --- a/src/Service/Zoho/Sync/Model/SyncInterface.php +++ /dev/null @@ -1,35 +0,0 @@ - - */ - public function getEntityName(): string; - - /** - * When an entity from @see getEntityName is updated, return iterable of messages to be executed after succesful flush. - * - * @no-named-arguments - * - * @param T $entity - * - * @return iterable - */ - public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable; - - public function downloadAll(Company $company): void; -} diff --git a/src/Service/Zoho/Sync/ProductSync.php b/src/Service/Zoho/Sync/ProductSync.php index 2927e26..d478ecd 100644 --- a/src/Service/Zoho/Sync/ProductSync.php +++ b/src/Service/Zoho/Sync/ProductSync.php @@ -7,110 +7,89 @@ use Override; use Money\Money; use Money\Currency; -use App\DTO\Zoho\Items; use App\Entity\Tax\Tax; -use InvalidArgumentException; use App\Entity\Product\Product; -use App\Entity\Company\Company; -use App\Service\Zoho\ZohoClient; use App\DTO\Zoho\Item as ZohoItem; use App\Entity\ZohoAwareInterface; +use App\DTO\Zoho\Items as ZohoItems; use App\Repository\Tax\TaxRepository; -use App\Message\Zoho\ZohoSyncProductMessage; +use App\Service\Zoho\Model\SyncInterface; +use App\Message\Zoho\ZohoPutEntityMessage; use App\Repository\Product\ProductRepository; -use App\Service\Zoho\Sync\Model\SyncInterface; -use Symfony\Component\Messenger\Attribute\AsMessageHandler; -use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use function is_float; use function is_string; use function strip_tags; use function array_key_exists; /** - * @implements SyncInterface + * @implements SyncInterface */ class ProductSync implements SyncInterface { public function __construct( - private ZohoClient $zohoClient, - private TaxRepository $repository, private ProductRepository $productRepository, + private TaxRepository $taxRepository, ) { } - #[AsMessageHandler] - public function __invoke(ZohoSyncProductMessage $message): void + #[Override] + public function map(ZohoAwareInterface $entity, object $mapping): void { - try { - if (!$product = $this->productRepository->find($message->getId())) { - return; - } - if (!is_string($zohoId = $product->getZohoId())) { - return; - } - // @see https://www.zoho.com/inventory/api/v1/items/#overview - $url = sprintf('/items/%s', $zohoId); - - match ($message->getAction()) { - 'remove' => $this->zohoClient->delete($product->getCompany(), $url), - 'update' => $this->zohoClient->put($product->getCompany(), $url, data: [ - 'rate' => (float)($product->getPrice()->getAmount()) / 100, - 'name' => $product->getName(), - 'description' => strip_tags($product->getDescription() ?? ''), - ]), - }; - } catch (InvalidArgumentException $e) { - throw new UnrecoverableMessageHandlingException(previous: $e); - } + $rate = $mapping->getRate(); + $rate = is_float($rate) ? $rate : 0; + $price = new Money((int)($rate * 100), new Currency('USD')); + $entity->setPrice($price); + + $entity->setName($mapping->getName()); + $entity->setDescription($mapping->getDescription()); + + $tax = $this->findTax($mapping); + $entity->setTax($tax); } #[Override] - public function getEntityName(): string + public function getBaseUrl(): string { - return Product::class; + return '/items'; } #[Override] - public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable + public function findEntityByZohoId(string $zohoId): Product|null { - if (!array_key_exists('name', $changeSet) && !array_key_exists('description', $changeSet) && !array_key_exists('price', $changeSet)) { - return; - } - - yield new ZohoSyncProductMessage($entity, 'update'); + return $this->productRepository->findOneBy(['zohoId' => $zohoId]); } #[Override] - public function downloadAll(Company $company): void + public function getMappingClass(): string { - $zohoItems = $this->zohoClient->get($company, '/items', Items::class); - foreach ($zohoItems->items as $zohoItem) { - $product = $this->getProduct($zohoItem, $company); - - $rate = $zohoItem->getRate(); - $rate = is_float($rate) ? $rate : 0; - $price = new Money((int)($rate * 100), new Currency('USD')); - $product->setPrice($price); - - $product->setName($zohoItem->getName()); - $product->setDescription($zohoItem->getDescription()); + return ZohoItems::class; + } - $tax = $this->findTax($zohoItem); - $product->setTax($tax); - } - $this->productRepository->flush(); + #[Override] + public function getEntityClass(): string + { + return Product::class; } - private function getProduct(ZohoItem $zohoItem, Company $company): Product + #[Override] + public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable { - if ($product = $this->productRepository->findOneBy(['zohoId' => $zohoItem->getItemId()])) { - return $product; + if (!array_key_exists('name', $changeSet) && !array_key_exists('value', $changeSet)) { + return; } - $product = new Product($company, name: $zohoItem->getName(), zohoId: (string)$zohoItem->getItemId()); - $this->productRepository->persist($product); - return $product; + yield new ZohoPutEntityMessage($entity, 'put'); + } + + #[Override] + public function createPutPayload(ZohoAwareInterface $entity): array + { + return [ + 'rate' => (float)($entity->getPrice()->getAmount()) / 100, + 'name' => $entity->getName(), + 'description' => strip_tags($entity->getDescription() ?? ''), + ]; } private function findTax(ZohoItem $zohoItem): ?Tax @@ -119,6 +98,6 @@ private function findTax(ZohoItem $zohoItem): ?Tax return null; } - return $this->repository->findOneBy(['zohoId' => $zohoTaxId]); + return $this->taxRepository->findOneBy(['zohoId' => $zohoTaxId]); } } diff --git a/src/Service/Zoho/Sync/TaxSync.php b/src/Service/Zoho/Sync/TaxSync.php index d423195..2c98542 100644 --- a/src/Service/Zoho/Sync/TaxSync.php +++ b/src/Service/Zoho/Sync/TaxSync.php @@ -5,56 +5,53 @@ namespace App\Service\Zoho\Sync; use Override; -use App\DTO\Zoho\Taxes; use App\Entity\Tax\Tax; -use App\Entity\Company\Company; use App\DTO\Zoho\Tax as ZohoTax; -use App\Service\Zoho\ZohoClient; use App\Entity\ZohoAwareInterface; +use App\DTO\Zoho\Taxes as ZohoTaxes; use App\Repository\Tax\TaxRepository; -use App\Message\Zoho\ZohoSyncTaxMessage; -use App\Service\Zoho\Sync\Model\SyncInterface; -use App\Message\Zoho\ZohoSyncWarehouseMessage; -use Symfony\Component\Messenger\Attribute\AsMessageHandler; -use function sprintf; -use function is_string; +use App\Service\Zoho\Model\SyncInterface; +use App\Message\Zoho\ZohoPutEntityMessage; use function array_key_exists; /** - * @implements SyncInterface + * @implements SyncInterface */ class TaxSync implements SyncInterface { public function __construct( - private ZohoClient $zohoClient, - private TaxRepository $repository, + private TaxRepository $taxRepository, ) { } - #[AsMessageHandler] - public function __invoke(ZohoSyncWarehouseMessage $message): void + #[Override] + public function map(ZohoAwareInterface $entity, object $mapping): void { - if (!$tax = $this->repository->find($message->getId())) { - return; - } - if (!is_string($zohoId = $tax->getZohoId())) { - return; - } - // @see https://www.zoho.com/inventory/api/v1/taxes/#overview - $url = sprintf('/settings/taxes/%s', $zohoId); + $entity->setName($mapping->getTaxName()); + $entity->setValue((int)$mapping->getTaxPercentage()); + } + + #[Override] + public function getBaseUrl(): string + { + return '/settings/taxes'; + } + + #[Override] + public function findEntityByZohoId(string $zohoId): Tax|null + { + return $this->taxRepository->findOneBy(['zohoId' => $zohoId]); + } - match ($message->getAction()) { - 'remove' => $this->zohoClient->delete($tax->getCompany(), $url), - 'update' => $this->zohoClient->put($tax->getCompany(), $url, data: [ - 'tax_name' => $tax->getName(), - 'tax_percentage' => $tax->getValue(), - ]), - }; + #[Override] + public function getMappingClass(): string + { + return ZohoTaxes::class; } #[Override] - public function getEntityName(): string + public function getEntityClass(): string { return Tax::class; } @@ -66,29 +63,15 @@ public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable return; } - yield new ZohoSyncTaxMessage($entity, 'update'); + yield new ZohoPutEntityMessage($entity, 'put'); } #[Override] - public function downloadAll(Company $company): void + public function createPutPayload(ZohoAwareInterface $entity): array { - $zohoTaxes = $this->zohoClient->get($company, '/settings/taxes', Taxes::class); - foreach ($zohoTaxes->taxes as $zohoTax) { - $tax = $this->getTax($zohoTax, $company); - $tax->setName($zohoTax->getTaxName()); - $tax->setValue((int)$zohoTax->getTaxPercentage()); - } - $this->repository->flush(); - } - - private function getTax(ZohoTax $zohoTax, Company $company): Tax - { - if ($tax = $this->repository->findOneBy(['zohoId' => $zohoTax->getTaxId()])) { - return $tax; - } - $tax = new Tax($company, $zohoTax->getTaxName(), $zohoTax->getTaxPercentage(), zohoId: $zohoTax->getTaxId()); - $this->repository->persist($tax); - - return $tax; + return [ + 'tax_name' => $entity->getName(), + 'tax_percentage' => $entity->getValue(), + ]; } } diff --git a/src/Service/Zoho/Sync/WarehouseSync.php b/src/Service/Zoho/Sync/WarehouseSync.php index 20db593..a2dfc07 100644 --- a/src/Service/Zoho/Sync/WarehouseSync.php +++ b/src/Service/Zoho/Sync/WarehouseSync.php @@ -5,86 +5,71 @@ namespace App\Service\Zoho\Sync; use Override; -use App\DTO\Zoho\Warehouses; -use App\Entity\Company\Company; -use App\Service\Zoho\ZohoClient; use App\Entity\ZohoAwareInterface; use App\Entity\Warehouse\Warehouse; +use App\Message\Zoho\ZohoPutEntityMessage; use App\DTO\Zoho\Warehouse as ZohoWarehouse; -use App\Service\Zoho\Sync\Model\SyncInterface; -use App\Message\Zoho\ZohoSyncWarehouseMessage; +use App\DTO\Zoho\Warehouses as ZohoWarehouses; use App\Repository\Warehouse\WarehouseRepository; -use Symfony\Component\Messenger\Attribute\AsMessageHandler; -use function is_string; +use App\Service\Zoho\Model\SyncInterface; use function array_key_exists; /** - * @implements SyncInterface + * @implements SyncInterface */ class WarehouseSync implements SyncInterface { public function __construct( - private ZohoClient $zohoClient, private WarehouseRepository $warehouseRepository, ) { } - #[AsMessageHandler] - public function __invoke(ZohoSyncWarehouseMessage $message): void + #[Override] + public function map(ZohoAwareInterface $entity, object $mapping): void { - if (!$warehouse = $this->warehouseRepository->find($message->getId())) { - return; - } - if (!is_string($zohoId = $warehouse->getZohoId())) { - return; - } - // @see https://www.zoho.com/inventory/api/v1/multi-warehouse/#overview - $url = sprintf('/settings/warehouses/%s', $zohoId); - - match ($message->getAction()) { - 'remove' => $this->zohoClient->delete($warehouse->getCompany(), $url), - 'update' => $this->zohoClient->put($warehouse->getCompany(), $url, data: [ - 'warehouse_name' => $warehouse->getName(), - ]), - }; + $entity->setName($mapping->getName()); } #[Override] - public function getEntityName(): string + public function getBaseUrl(): string { - return Warehouse::class; + return '/settings/warehouses'; } #[Override] - public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable + public function findEntityByZohoId(string $zohoId): Warehouse|null { - if (!array_key_exists('name', $changeSet)) { - return; - } + return $this->warehouseRepository->findOneBy(['zohoId' => $zohoId]); + } - yield new ZohoSyncWarehouseMessage($entity, 'update'); + #[Override] + public function getMappingClass(): string + { + return ZohoWarehouses::class; } #[Override] - public function downloadAll(Company $company): void + public function getEntityClass(): string { - $warehouses = $this->zohoClient->get($company, '/settings/warehouses', Warehouses::class); - foreach ($warehouses->warehouses as $zohoWarehouse) { - $warehouse = $this->getWarehouse($zohoWarehouse, $company); - $warehouse->setName($zohoWarehouse->getName()); - } - $this->warehouseRepository->flush(); + return Warehouse::class; } - private function getWarehouse(ZohoWarehouse $zohoWarehouse, Company $company): Warehouse + #[Override] + public function onUpdate(ZohoAwareInterface $entity, array $changeSet): iterable { - if ($product = $this->warehouseRepository->findOneBy(['zohoId' => $zohoWarehouse->getWarehouseId()])) { - return $product; + if (!array_key_exists('name', $changeSet) && !array_key_exists('value', $changeSet)) { + return; } - $warehouse = new Warehouse($company, $zohoWarehouse->getName(), zohoId: $zohoWarehouse->getWarehouseId()); - $this->warehouseRepository->persist($warehouse); - return $warehouse; + yield new ZohoPutEntityMessage($entity, 'put'); + } + + #[Override] + public function createPutPayload(ZohoAwareInterface $entity): array + { + return [ + 'warehouse_name' => $entity->getName(), + ]; } } diff --git a/src/Service/Zoho/ZohoClient.php b/src/Service/Zoho/ZohoClient.php index b6e8bd5..1700c4f 100644 --- a/src/Service/Zoho/ZohoClient.php +++ b/src/Service/Zoho/ZohoClient.php @@ -92,6 +92,39 @@ public function delete(Company $company, string $url): void $this->request($company, 'DELETE', $url); } + public function getConnectUrl(): string + { + return sprintf('%s/oauth/v2/auth?scope=%s&client_id=%s&state=testing&response_type=code&redirect_uri=%s&access_type=offline', + $this->baseUrl, + 'ZohoInventory.FullAccess.all', + $this->clientId, + $this->generateRedirectUrl(), + ); + } + + public function getToken(string $code, Company $company): void + { + $url = sprintf('%s/oauth/v2/token?code=%s&client_id=%s&client_secret=%s&redirect_uri=%s&grant_type=authorization_code', + $this->baseUrl, + $code, + $this->clientId, + $this->clientSecret, + $this->generateRedirectUrl(), + ); + + $response = $this->httpClient->request('POST', $url); + $data = $response->getContent(); + $dto = $this->treeMapper->map(Token::class, Source::json($data)->camelCaseKeys()); + + $company->setZohoAccessToken($dto->getAccessToken()); + $company->setZohoRefreshToken($dto->getRefreshToken()); + if (is_int($expiresIn = $dto->getExpiresIn())) { + $expiresAt = new DateTimeImmutable('+' . $expiresIn . ' seconds'); + $company->setZohoExpiresAt($expiresAt); + } + $this->tokenRefreshed = true; + } + /** * @param non-empty-array|null $payload * @@ -129,39 +162,6 @@ private function request(Company $company, string $method, string $url, ?array $ return $response->getContent(false); } - public function getConnectUrl(): string - { - return sprintf('%s/oauth/v2/auth?scope=%s&client_id=%s&state=testing&response_type=code&redirect_uri=%s&access_type=offline', - $this->baseUrl, - 'ZohoInventory.FullAccess.all', - $this->clientId, - $this->generateRedirectUrl(), - ); - } - - public function getToken(string $code, Company $company): void - { - $url = sprintf('%s/oauth/v2/token?code=%s&client_id=%s&client_secret=%s&redirect_uri=%s&grant_type=authorization_code', - $this->baseUrl, - $code, - $this->clientId, - $this->clientSecret, - $this->generateRedirectUrl(), - ); - - $response = $this->httpClient->request('POST', $url); - $data = $response->getContent(); - $dto = $this->treeMapper->map(Token::class, Source::json($data)->camelCaseKeys()); - - $company->setZohoAccessToken($dto->getAccessToken()); - $company->setZohoRefreshToken($dto->getRefreshToken()); - if (is_int($expiresIn = $dto->getExpiresIn())) { - $expiresAt = new DateTimeImmutable('+' . $expiresIn . ' seconds'); - $company->setZohoExpiresAt($expiresAt); - } - $this->tokenRefreshed = true; - } - private function refreshToken(Company $company): string { $url = sprintf('%s/oauth/v2/token?refresh_token=%s&client_id=%s&client_secret=%s&grant_type=refresh_token', diff --git a/src/Service/Zoho/ZohoManager.php b/src/Service/ZohoImprovedManager.php similarity index 61% rename from src/Service/Zoho/ZohoManager.php rename to src/Service/ZohoImprovedManager.php index 4b50c8e..3ee7cad 100644 --- a/src/Service/Zoho/ZohoManager.php +++ b/src/Service/ZohoImprovedManager.php @@ -2,21 +2,22 @@ declare(strict_types=1); -namespace App\Service\Zoho; +namespace App\Service; use Override; use Generator; use App\Entity\Company\Company; -use App\Message\ZohoSyncMessage; -use App\Service\Zoho\Sync\TaxSync; +use App\Service\Zoho\ZohoClient; use App\Entity\ZohoAwareInterface; -use App\Service\Zoho\Sync\ProductSync; +use App\Service\Zoho\Sync\TaxSync; use App\Message\AsyncMessageInterface; +use App\Service\Zoho\Sync\ProductSync; use App\Service\Zoho\Sync\WarehouseSync; +use App\Service\Zoho\Model\SyncInterface; use Doctrine\ORM\Event\PreUpdateEventArgs; +use App\Message\Zoho\ZohoPutEntityMessage; use App\Repository\Company\CompanyRepository; use Symfony\Contracts\Service\ResetInterface; -use App\Service\Zoho\Sync\Model\SyncInterface; use Symfony\Component\Messenger\Stamp\DelayStamp; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -29,7 +30,7 @@ use function sprintf; use function array_keys; -class ZohoManager implements ResetInterface, PreUpdateEventListenerInterface, PostFlushEventListenerInterface +class ZohoImprovedManager implements ResetInterface, PreUpdateEventListenerInterface, PostFlushEventListenerInterface { private bool $syncEnabled = true; @@ -46,16 +47,25 @@ public function __construct( private ServiceLocator $syncs, private CompanyRepository $companyRepository, private MessageBusInterface $messageBus, + private ZohoClient $zohoClient, ) { } #[AsMessageHandler] - public function __invoke(ZohoSyncMessage $message): void + public function __invoke(ZohoPutEntityMessage $message): void { - $id = $message->getId(); - $company = $this->companyRepository->find($id) ?? throw new UnrecoverableMessageHandlingException(sprintf('Company %s does not exist.', $id)); - $this->downloadAll($company); + $entityClassName = $message->getClass(); + $sync = $this->findSyncForEntity($entityClassName) ?? throw new UnrecoverableMessageHandlingException(sprintf('Entity %s not supported', $entityClassName)); + $zohoId = $message->getZohoId(); + if (!$entity = $sync->findEntityByZohoId($zohoId)) { + return; + } + $url = $sync->getBaseUrl() . '/' . $zohoId; + match ($message->getAction()) { + 'delete' => $this->zohoClient->delete($entity->getCompany(), $url), + 'put' => $this->zohoClient->put($entity->getCompany(), $url, data: $sync->createPutPayload($entity)), + }; } #[Override] @@ -74,10 +84,17 @@ public function downloadAll(Company $company): void $this->syncEnabled = false; foreach ($this->getSyncsInOrder() as $serviceName) { $sync = $this->syncs->get($serviceName); - $sync->downloadAll($company); + $mapperClass = $sync->getMappingClass(); + $data = $this->zohoClient->get($company, $sync->getBaseUrl(), $mapperClass); + foreach ($data->getMany() as $item) { + $zohoId = $item->getId(); + if ($entity = $sync->findEntityByZohoId($zohoId)) { + $sync->map($entity, $item); + } + } + // in case tagged service didn't flush its entities, do it here + $this->companyRepository->flush(); } - // in case tagged service didn't flush its entities, do it here - $this->companyRepository->flush(); $this->syncEnabled = true; } @@ -93,14 +110,11 @@ public function preUpdate(PreUpdateEventArgs $event): void if (!$entity instanceof ZohoAwareInterface) { return; } - - foreach ($this->getAllSyncServices() as $sync) { - if (!is_a($entity, $sync->getEntityName(), true)) { - continue; - } - foreach ($sync->onUpdate($entity, $changeSet) as $message) { - $this->pendingUpdateMessages[] = $message; - } + if (!$sync = $this->findSyncForEntity($entity)) { + return; + } + foreach ($sync->onUpdate($entity, $changeSet) as $message) { + $this->pendingUpdateMessages[] = $message; } } @@ -126,6 +140,23 @@ private function getAllSyncServices(): Generator } } + /** + * @template TEntity of ZohoAwareInterface + * + * @param class-string|TEntity $entityClassName + */ + private function findSyncForEntity(string|object $entityClassName): SyncInterface|null + { + foreach ($this->getSyncsInOrder() as $serviceName) { + $sync = $this->syncs->get($serviceName); + if (is_a($entityClassName, $sync->getEntityClass(), true)) { + return $sync; + } + } + + return null; + } + /** * We need to download data from Zoho in specific order so proper DB relations can be established. * From 70f9b97214353e8490c075359a541f7da93978d8 Mon Sep 17 00:00:00 2001 From: zeljko Date: Mon, 8 Apr 2024 20:47:36 +0200 Subject: [PATCH 3/3] Improved sync --- src/Form/Entity/Product/ProductType.php | 1 - src/Message/Zoho/ZohoSyncProductMessage.php | 38 ------------------- src/Message/Zoho/ZohoSyncTaxMessage.php | 38 ------------------- src/Message/Zoho/ZohoSyncWarehouseMessage.php | 38 ------------------- src/Message/ZohoSyncMessage.php | 22 ----------- src/Service/ZohoImprovedManager.php | 12 ------ 6 files changed, 149 deletions(-) delete mode 100644 src/Message/Zoho/ZohoSyncProductMessage.php delete mode 100644 src/Message/Zoho/ZohoSyncTaxMessage.php delete mode 100644 src/Message/Zoho/ZohoSyncWarehouseMessage.php delete mode 100644 src/Message/ZohoSyncMessage.php diff --git a/src/Form/Entity/Product/ProductType.php b/src/Form/Entity/Product/ProductType.php index 88f19ff..f16bb96 100644 --- a/src/Form/Entity/Product/ProductType.php +++ b/src/Form/Entity/Product/ProductType.php @@ -54,7 +54,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder->add('tax', EntityType::class, [ 'class' => Tax::class, - 'disabled' => true, 'required' => false, 'placeholder' => '--', 'get_value' => fn(Product $product) => $product->getTax(), diff --git a/src/Message/Zoho/ZohoSyncProductMessage.php b/src/Message/Zoho/ZohoSyncProductMessage.php deleted file mode 100644 index 34d7512..0000000 --- a/src/Message/Zoho/ZohoSyncProductMessage.php +++ /dev/null @@ -1,38 +0,0 @@ -id = $product->getId(); - } - - public function getId(): string - { - return $this->id; - } - - /** - * @return TAction - */ - public function getAction(): string - { - return $this->action; - } -} diff --git a/src/Message/Zoho/ZohoSyncTaxMessage.php b/src/Message/Zoho/ZohoSyncTaxMessage.php deleted file mode 100644 index 844876e..0000000 --- a/src/Message/Zoho/ZohoSyncTaxMessage.php +++ /dev/null @@ -1,38 +0,0 @@ -id = $tax->getId(); - } - - public function getId(): string - { - return $this->id; - } - - /** - * @return TAction - */ - public function getAction(): string - { - return $this->action; - } -} diff --git a/src/Message/Zoho/ZohoSyncWarehouseMessage.php b/src/Message/Zoho/ZohoSyncWarehouseMessage.php deleted file mode 100644 index a28031d..0000000 --- a/src/Message/Zoho/ZohoSyncWarehouseMessage.php +++ /dev/null @@ -1,38 +0,0 @@ -id = $warehouse->getId(); - } - - public function getId(): string - { - return $this->id; - } - - /** - * @return TAction - */ - public function getAction(): string - { - return $this->action; - } -} diff --git a/src/Message/ZohoSyncMessage.php b/src/Message/ZohoSyncMessage.php deleted file mode 100644 index dae5659..0000000 --- a/src/Message/ZohoSyncMessage.php +++ /dev/null @@ -1,22 +0,0 @@ -id = $company->getId(); - } - - public function getId(): string - { - return $this->id; - } -} diff --git a/src/Service/ZohoImprovedManager.php b/src/Service/ZohoImprovedManager.php index 3ee7cad..a897482 100644 --- a/src/Service/ZohoImprovedManager.php +++ b/src/Service/ZohoImprovedManager.php @@ -28,7 +28,6 @@ use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use function is_a; use function sprintf; -use function array_keys; class ZohoImprovedManager implements ResetInterface, PreUpdateEventListenerInterface, PostFlushEventListenerInterface { @@ -129,17 +128,6 @@ public function postFlush(): void $this->pendingUpdateMessages = []; } - /** - * @return Generator - */ - private function getAllSyncServices(): Generator - { - $identifiers = array_keys($this->syncs->getProvidedServices()); - foreach ($identifiers as $identifier) { - yield $this->syncs->get($identifier); - } - } - /** * @template TEntity of ZohoAwareInterface *