Skip to content

Commit

Permalink
Merge pull request #106 from Setono/refactor-handler
Browse files Browse the repository at this point in the history
Refactor the assign callouts handler
  • Loading branch information
loevgaard authored Jan 25, 2024
2 parents 8ce2370 + 397ca6a commit 7ee994a
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 96 deletions.
6 changes: 5 additions & 1 deletion src/EventSubscriber/HandleCalloutUpdateSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
*/
final class HandleCalloutUpdateSubscriber implements EventSubscriberInterface
{
/** @var array<string, bool> */
/**
* The keys are the callout codes and the values are true
*
* @var array<string, bool>
*/
private array $calloutsToAssign = [];

public function __construct(private readonly MessageBusInterface $commandBus)
Expand Down
78 changes: 78 additions & 0 deletions src/Message/Handler/AbstractAssignCalloutsHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusCalloutPlugin\Message\Handler;

use Doctrine\Persistence\ManagerRegistry;
use DoctrineBatchUtils\BatchProcessing\SimpleBatchIteratorAggregate;
use Psr\EventDispatcher\EventDispatcherInterface;
use Setono\DoctrineObjectManagerTrait\ORM\ORMManagerTrait;
use Setono\SyliusCalloutPlugin\Checker\Eligibility\CalloutEligibilityCheckerInterface;
use Setono\SyliusCalloutPlugin\Event\ProductQueryBuilderEvent;
use Setono\SyliusCalloutPlugin\Model\CalloutInterface;
use Setono\SyliusCalloutPlugin\Model\ProductInterface;
use Setono\SyliusCalloutPlugin\Repository\CalloutRepositoryInterface;

class AbstractAssignCalloutsHandler
{
use ORMManagerTrait;

public function __construct(
protected readonly CalloutRepositoryInterface $calloutRepository,
protected readonly EventDispatcherInterface $eventDispatcher,
protected readonly CalloutEligibilityCheckerInterface $eligibilityChecker,
ManagerRegistry $managerRegistry,
/** @var class-string $productClass */
protected readonly string $productClass,
) {
$this->managerRegistry = $managerRegistry;
}

/**
* @param iterable<ProductInterface> $products If the $products argument is empty, all products will be fetched from the database
* @param iterable<CalloutInterface> $callouts If the $callouts argument is empty, all callouts will be fetched from the database
*/
protected function assign(iterable $products = [], iterable $callouts = []): void
{
$manager = $this->getManager($this->productClass);

if ([] === $products) {
$qb = $manager
->createQueryBuilder()
->select('o')
->from($this->productClass, 'o')
// todo the following two 'wheres' should be moved to event subscribers and be able to be disabled in the configuration of the plugin
->andWhere('o.enabled = true')
->andWhere('SIZE(o.channels) > 0')
;

$this->eventDispatcher->dispatch(new ProductQueryBuilderEvent($qb));

/** @var iterable<ProductInterface> $products */
$products = SimpleBatchIteratorAggregate::fromQuery($qb->getQuery(), 100);
}

$resetCallouts = false;
// we know that the only case where we want to reset the callouts is when we assign _all_ callouts
if ([] === $callouts) {
$callouts = $this->calloutRepository->findEnabled();
$resetCallouts = true;
}

foreach ($products as $product) {
if ($resetCallouts) {
$product->resetPreQualifiedCallouts();
}

foreach ($callouts as $callout) {
$product->removePreQualifiedCallout($callout);
if ($this->eligibilityChecker->isEligible($product, $callout)) {
$product->addPreQualifiedCallout($callout);
}
}
}

$manager->flush();
}
}
32 changes: 32 additions & 0 deletions src/Message/Handler/AssignCalloutHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusCalloutPlugin\Message\Handler;

use Setono\SyliusCalloutPlugin\Message\Command\AssignCallout;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;

final class AssignCalloutHandler extends AbstractAssignCalloutsHandler
{
public function __invoke(AssignCallout $message): void
{
$callout = $this->calloutRepository->findOneByCode($message->callout);
if (null === $callout) {
throw new UnrecoverableMessageHandlingException(sprintf('Callout with code "%s" does not exist', $message->callout));
}

if (null !== $message->version && $message->version !== $callout->getVersion()) {
// this means the callout has been updated since the message was sent
throw new UnrecoverableMessageHandlingException(sprintf('Callout with id %s has version %s, but version %s was expected', $message->callout, (string) $callout->getVersion(), $message->version));
}

if (!$callout->isEnabled()) {
throw new UnrecoverableMessageHandlingException(sprintf('Callout with id %s is not enabled', $message->callout));
}

$callouts = [$callout];

$this->assign(callouts: $callouts);
}
}
98 changes: 6 additions & 92 deletions src/Message/Handler/AssignCalloutsHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,103 +4,17 @@

namespace Setono\SyliusCalloutPlugin\Message\Handler;

use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use DoctrineBatchUtils\BatchProcessing\SimpleBatchIteratorAggregate;
use Psr\EventDispatcher\EventDispatcherInterface;
use Setono\DoctrineObjectManagerTrait\ORM\ORMManagerTrait;
use Setono\SyliusCalloutPlugin\Checker\Eligibility\CalloutEligibilityCheckerInterface;
use Setono\SyliusCalloutPlugin\Event\ProductQueryBuilderEvent;
use Setono\SyliusCalloutPlugin\Message\Command\AssignCallout;
use Setono\SyliusCalloutPlugin\Message\Command\AssignCallouts;
use Setono\SyliusCalloutPlugin\Message\Command\AssignCalloutsToProduct;
use Setono\SyliusCalloutPlugin\Model\ProductInterface;
use Setono\SyliusCalloutPlugin\Repository\CalloutRepositoryInterface;
use Sylius\Component\Core\Repository\ProductRepositoryInterface;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Webmozart\Assert\Assert;

// todo this handler should be split into three handlers, one for each message
final class AssignCalloutsHandler
final class AssignCalloutsHandler extends AbstractAssignCalloutsHandler
{
use ORMManagerTrait;

public function __construct(
private readonly CalloutRepositoryInterface $calloutRepository,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly CalloutEligibilityCheckerInterface $eligibilityChecker,
ManagerRegistry $managerRegistry,
/** @var class-string $productClass */
private readonly string $productClass,
) {
$this->managerRegistry = $managerRegistry;
}

public function __invoke(AssignCallouts|AssignCallout|AssignCalloutsToProduct $message): void
public function __invoke(AssignCallouts $message): void
{
if ($message instanceof AssignCallout) {
$callout = $this->calloutRepository->findOneByCode($message->callout);
if (null === $callout) {
throw new UnrecoverableMessageHandlingException(sprintf('Callout with code "%s" does not exist', $message->callout));
}

if (null !== $message->version && $message->version !== $callout->getVersion()) {
// this means the callout has been updated since the message was sent
throw new UnrecoverableMessageHandlingException(sprintf('Callout with id %s has version %s, but version %s was expected', $message->callout, (string) $callout->getVersion(), $message->version));
}

if (!$callout->isEnabled()) {
throw new UnrecoverableMessageHandlingException(sprintf('Callout with id %s is not enabled', $message->callout));
}

$callouts = [$callout];
} else {
$callouts = $this->calloutRepository->findEnabled($message instanceof AssignCallouts ? $message->callouts : []);
}

$manager = $this->getManager($this->productClass);
if ($message instanceof AssignCalloutsToProduct) {
/** @var ProductRepositoryInterface|EntityRepository $productRepository */
$productRepository = $manager->getRepository($this->productClass);
Assert::isInstanceOf($productRepository, ProductRepositoryInterface::class);

$product = $productRepository->findOneByCode($message->product);
if (!$product instanceof ProductInterface) {
throw new UnrecoverableMessageHandlingException(sprintf('Product with code "%s" does not exist', $message->product));
}

$products = [$product];
} else {
$qb = $manager
->createQueryBuilder()
->select('o')
->from($this->productClass, 'o')
// todo the following two 'wheres' should be moved to event subscribers and be able to be disabled in the configuration of the plugin
->andWhere('o.enabled = true')
->andWhere('SIZE(o.channels) > 0')
;

$this->eventDispatcher->dispatch(new ProductQueryBuilderEvent($qb));

/** @var list<ProductInterface> $products */
$products = SimpleBatchIteratorAggregate::fromQuery($qb->getQuery(), 100);
}

foreach ($products as $product) {
// we only want to reset callouts if we are assigning all callouts
match ($message::class) {
AssignCallouts::class => [] === $message->callouts ? $product->resetPreQualifiedCallouts() : array_map(static fn (string $callout) => $product->removePreQualifiedCallout($callout), $message->callouts),
AssignCallout::class => $product->removePreQualifiedCallout($message->callout),
AssignCalloutsToProduct::class => $product->resetPreQualifiedCallouts(),
};

foreach ($callouts as $callout) {
if ($this->eligibilityChecker->isEligible($product, $callout)) {
$product->addPreQualifiedCallout($callout);
}
}
$callouts = [];
if ([] !== $message->callouts) {
$callouts = $this->calloutRepository->findEnabled($message->callouts);
}

$manager->flush();
$this->assign(callouts: $callouts);
}
}
33 changes: 33 additions & 0 deletions src/Message/Handler/AssignCalloutsToProductHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusCalloutPlugin\Message\Handler;

use Doctrine\ORM\EntityRepository;
use Setono\SyliusCalloutPlugin\Message\Command\AssignCalloutsToProduct;
use Setono\SyliusCalloutPlugin\Model\ProductInterface;
use Sylius\Component\Core\Repository\ProductRepositoryInterface;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Webmozart\Assert\Assert;

final class AssignCalloutsToProductHandler extends AbstractAssignCalloutsHandler
{
public function __invoke(AssignCalloutsToProduct $message): void
{
$manager = $this->getManager($this->productClass);

/** @var ProductRepositoryInterface|EntityRepository $productRepository */
$productRepository = $manager->getRepository($this->productClass);
Assert::isInstanceOf($productRepository, ProductRepositoryInterface::class);

$product = $productRepository->findOneByCode($message->product);
if (!$product instanceof ProductInterface) {
throw new UnrecoverableMessageHandlingException(sprintf('Product with code "%s" does not exist', $message->product));
}

$products = [$product];

$this->assign($products);
}
}
2 changes: 1 addition & 1 deletion src/Model/ProductInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public function getPreQualifiedCallouts(): array;

public function addPreQualifiedCallout(CalloutInterface|string $preQualifiedCallout): void;

public function removePreQualifiedCallout(string $preQualifiedCallout): void;
public function removePreQualifiedCallout(CalloutInterface|string $preQualifiedCallout): void;

/**
* Resets the pre-qualified callouts
Expand Down
6 changes: 5 additions & 1 deletion src/Model/ProductTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ public function addPreQualifiedCallout(CalloutInterface|string $preQualifiedCall
}
}

public function removePreQualifiedCallout(string $preQualifiedCallout): void
public function removePreQualifiedCallout(CalloutInterface|string $preQualifiedCallout): void
{
if (null === $this->preQualifiedCallouts) {
return;
}

if ($preQualifiedCallout instanceof CalloutInterface) {
$preQualifiedCallout = (string) $preQualifiedCallout->getCode();
}

$key = array_search($preQualifiedCallout, $this->preQualifiedCallouts, true);
if (false !== $key) {
unset($this->preQualifiedCallouts[$key]);
Expand Down
19 changes: 18 additions & 1 deletion src/Resources/config/services/message.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,30 @@
xmlns="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Setono\SyliusCalloutPlugin\Message\Handler\AssignCalloutsHandler">
<service id="setono_sylius_callout.message.handler.abstract_assign_callouts"
class="Setono\SyliusCalloutPlugin\Message\Handler\AbstractAssignCalloutsHandler" abstract="true">
<argument type="service" id="setono_sylius_callout.repository.callout"/>
<argument type="service" id="event_dispatcher"/>
<argument type="service" id="setono_sylius_callout.checker.eligibility.composite"/>
<argument type="service" id="doctrine"/>
<argument>%sylius.model.product.class%</argument>
</service>

<service id="setono_sylius_callout.message.handler.assign_callout"
class="Setono\SyliusCalloutPlugin\Message\Handler\AssignCalloutHandler"
parent="setono_sylius_callout.message.handler.abstract_assign_callouts">
<tag name="messenger.message_handler"/>
</service>

<service id="setono_sylius_callout.message.handler.assign_callouts"
class="Setono\SyliusCalloutPlugin\Message\Handler\AssignCalloutsHandler"
parent="setono_sylius_callout.message.handler.abstract_assign_callouts">
<tag name="messenger.message_handler"/>
</service>

<service id="setono_sylius_callout.message.handler.assign_callouts_to_product"
class="Setono\SyliusCalloutPlugin\Message\Handler\AssignCalloutsToProductHandler"
parent="setono_sylius_callout.message.handler.abstract_assign_callouts">
<tag name="messenger.message_handler"/>
</service>
</services>
Expand Down

0 comments on commit 7ee994a

Please sign in to comment.