diff --git a/README.md b/README.md index ce6d1eb..d3a792f 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ in `config/bundles.php` file of your project before (!) `SyliusGridBundle`: $bundles = [ Setono\ClientIdBundle\SetonoClientIdBundle::class => ['all' => true], Setono\ConsentBundle\SetonoConsentBundle::class => ['all' => true], + Setono\BotDetectionBundle\SetonoBotDetectionBundle::class => ['all' => true], Setono\SyliusFacebookPlugin\SetonoSyliusFacebookPlugin::class => ['all' => true], Sylius\Bundle\GridBundle\SyliusGridBundle::class => ['all' => true], ]; diff --git a/UPGRADE.md b/UPGRADE.md index cd63d5e..b103c80 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -3,7 +3,7 @@ 1. As we're moving to server side tracking - we no longer need `SetonoTagBagBundle` and `SetonoSyliusTagBagPlugin`. -- Remove them from `config/bundles.php`: +- Remove them from `config/bundles.php` and add ones we're using: ```diff $bundles = [ @@ -11,6 +11,7 @@ - Setono\SyliusTagBagPlugin\SetonoSyliusTagBagPlugin::class => ['all' => true], + Setono\ClientIdBundle\SetonoClientIdBundle::class => ['all' => true], + Setono\ConsentBundle\SetonoConsentBundle::class => ['all' => true], + + Setono\BotDetectionBundle\SetonoBotDetectionBundle::class => ['all' => true], Setono\SyliusFacebookPlugin\SetonoSyliusFacebookPlugin::class => ['all' => true], Sylius\Bundle\GridBundle\SyliusGridBundle::class => ['all' => true], diff --git a/composer.json b/composer.json index f6e9ff9..36ffba4 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "fzaninotto/faker": "^1.6", "knplabs/knp-menu": "^3.0", "psr/log": "^1.1", + "setono/bot-detection-bundle": "^1.1", "setono/client-id-bundle": "^0.2", "setono/client-id-contracts": "^0.2", "setono/consent-bundle": "^0.1", diff --git a/src/EventListener/AbstractSubscriber.php b/src/EventListener/AbstractSubscriber.php index 1de29f2..1f35d98 100644 --- a/src/EventListener/AbstractSubscriber.php +++ b/src/EventListener/AbstractSubscriber.php @@ -4,11 +4,9 @@ namespace Setono\SyliusFacebookPlugin\EventListener; -use Doctrine\ORM\EntityManagerInterface; +use Setono\BotDetectionBundle\BotDetector\BotDetectorInterface; use Setono\SyliusFacebookPlugin\Context\PixelContextInterface; -use Setono\SyliusFacebookPlugin\DataMapper\DataMapperInterface; -use Setono\SyliusFacebookPlugin\Factory\PixelEventFactoryInterface; -use Setono\SyliusFacebookPlugin\ServerSide\ServerSideEventFactoryInterface; +use Setono\SyliusFacebookPlugin\Generator\PixelEventsGeneratorInterface; use Symfony\Bundle\SecurityBundle\Security\FirewallMap; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; @@ -16,41 +14,28 @@ abstract class AbstractSubscriber implements EventSubscriberInterface { - protected PixelContextInterface $pixelContext; - protected RequestStack $requestStack; protected FirewallMap $firewallMap; - protected ServerSideEventFactoryInterface $serverSideFactory; - - protected DataMapperInterface $dataMapper; + protected PixelContextInterface $pixelContext; - protected PixelEventFactoryInterface $pixelEventFactory; + protected PixelEventsGeneratorInterface $pixelEventsGenerator; - protected EntityManagerInterface $entityManager; + protected BotDetectorInterface $botDetector; public function __construct( - PixelContextInterface $pixelContext, RequestStack $requestStack, FirewallMap $firewallMap, - ServerSideEventFactoryInterface $serverSideFactory, - DataMapperInterface $dataMapper, - PixelEventFactoryInterface $pixelEventFactory, - EntityManagerInterface $entityManager + PixelContextInterface $pixelContext, + PixelEventsGeneratorInterface $pixelEventsGenerator, + BotDetectorInterface $botDetector ) { - $this->pixelContext = $pixelContext; $this->requestStack = $requestStack; $this->firewallMap = $firewallMap; - $this->serverSideFactory = $serverSideFactory; - $this->dataMapper = $dataMapper; - $this->pixelEventFactory = $pixelEventFactory; - $this->entityManager = $entityManager; - } - - protected function getMasterRequest(): ?Request - { - return $this->requestStack->getMasterRequest(); + $this->pixelContext = $pixelContext; + $this->pixelEventsGenerator = $pixelEventsGenerator; + $this->botDetector = $botDetector; } protected function isShopContext(Request $request = null): bool @@ -70,26 +55,24 @@ protected function isShopContext(Request $request = null): bool return $firewallConfig->getName() === 'shop'; } - /** - * @param object $source - */ - protected function generatePixelEvents($source, string $eventName, Request $request = null): void + protected function isRequestEligible(): bool { - $serverSideEvent = $this->serverSideFactory->create($eventName); - $this->dataMapper->map($source, $serverSideEvent, [ - 'request' => $request ?? $this->getMasterRequest(), - 'event' => $eventName, - ]); - - $pixels = $this->pixelContext->getPixels(); - foreach ($pixels as $pixel) { - // @todo Maybe its better to just clone - $pixelEvent = $this->pixelEventFactory->createFromServerSideEvent($serverSideEvent); - $pixelEvent->setPixel($pixel); - - $this->entityManager->persist($pixelEvent); + // As one main request can have multiple subrequests + // we don't want things to be tracked multiple times + // So, having in mind that `If current Request is the master request, it returns null` + // we expect getParentRequest to be null to proceed + if (null !== $this->requestStack->getParentRequest()) { + return false; + } + + if (!$this->isShopContext()) { + return false; + } + + if ($this->botDetector->isBotRequest()) { + return false; } - $this->entityManager->flush(); + return true; } } diff --git a/src/EventListener/AddToCartSubscriber.php b/src/EventListener/AddToCartSubscriber.php index dfa858b..cba47a0 100644 --- a/src/EventListener/AddToCartSubscriber.php +++ b/src/EventListener/AddToCartSubscriber.php @@ -4,11 +4,9 @@ namespace Setono\SyliusFacebookPlugin\EventListener; -use Doctrine\ORM\EntityManagerInterface; +use Setono\BotDetectionBundle\BotDetector\BotDetectorInterface; use Setono\SyliusFacebookPlugin\Context\PixelContextInterface; -use Setono\SyliusFacebookPlugin\DataMapper\DataMapperInterface; -use Setono\SyliusFacebookPlugin\Factory\PixelEventFactoryInterface; -use Setono\SyliusFacebookPlugin\ServerSide\ServerSideEventFactoryInterface; +use Setono\SyliusFacebookPlugin\Generator\PixelEventsGeneratorInterface; use Setono\SyliusFacebookPlugin\ServerSide\ServerSideEventInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Order\Context\CartContextInterface; @@ -20,23 +18,19 @@ final class AddToCartSubscriber extends AbstractSubscriber protected CartContextInterface $cartContext; public function __construct( - PixelContextInterface $pixelContext, RequestStack $requestStack, FirewallMap $firewallMap, - ServerSideEventFactoryInterface $serverSideFactory, - DataMapperInterface $dataMapper, - PixelEventFactoryInterface $pixelEventFactory, - EntityManagerInterface $entityManager, + PixelContextInterface $pixelContext, + PixelEventsGeneratorInterface $pixelEventsGenerator, + BotDetectorInterface $botDetector, CartContextInterface $cartContext ) { parent::__construct( - $pixelContext, $requestStack, $firewallMap, - $serverSideFactory, - $dataMapper, - $pixelEventFactory, - $entityManager + $pixelContext, + $pixelEventsGenerator, + $botDetector ); $this->cartContext = $cartContext; @@ -53,7 +47,7 @@ public static function getSubscribedEvents(): array public function track(): void { - if (!$this->isShopContext() || !$this->pixelContext->hasPixels()) { + if (!$this->isRequestEligible() || !$this->pixelContext->hasPixels()) { return; } @@ -62,7 +56,7 @@ public function track(): void return; } - $this->generatePixelEvents( + $this->pixelEventsGenerator->generatePixelEvents( $order, ServerSideEventInterface::EVENT_ADD_TO_CART ); diff --git a/src/EventListener/InitiateCheckoutSubscriber.php b/src/EventListener/InitiateCheckoutSubscriber.php index 0611caf..61058a0 100644 --- a/src/EventListener/InitiateCheckoutSubscriber.php +++ b/src/EventListener/InitiateCheckoutSubscriber.php @@ -4,11 +4,9 @@ namespace Setono\SyliusFacebookPlugin\EventListener; -use Doctrine\ORM\EntityManagerInterface; +use Setono\BotDetectionBundle\BotDetector\BotDetectorInterface; use Setono\SyliusFacebookPlugin\Context\PixelContextInterface; -use Setono\SyliusFacebookPlugin\DataMapper\DataMapperInterface; -use Setono\SyliusFacebookPlugin\Factory\PixelEventFactoryInterface; -use Setono\SyliusFacebookPlugin\ServerSide\ServerSideEventFactoryInterface; +use Setono\SyliusFacebookPlugin\Generator\PixelEventsGeneratorInterface; use Setono\SyliusFacebookPlugin\ServerSide\ServerSideEventInterface; use Sylius\Component\Order\Context\CartContextInterface; use Symfony\Bundle\SecurityBundle\Security\FirewallMap; @@ -21,23 +19,19 @@ final class InitiateCheckoutSubscriber extends AbstractSubscriber protected CartContextInterface $cartContext; public function __construct( - PixelContextInterface $pixelContext, RequestStack $requestStack, FirewallMap $firewallMap, - ServerSideEventFactoryInterface $serverSideFactory, - DataMapperInterface $dataMapper, - PixelEventFactoryInterface $pixelEventFactory, - EntityManagerInterface $entityManager, + PixelContextInterface $pixelContext, + PixelEventsGeneratorInterface $pixelEventsGenerator, + BotDetectorInterface $botDetector, CartContextInterface $cartContext ) { parent::__construct( - $pixelContext, $requestStack, $firewallMap, - $serverSideFactory, - $dataMapper, - $pixelEventFactory, - $entityManager + $pixelContext, + $pixelEventsGenerator, + $botDetector ); $this->cartContext = $cartContext; @@ -52,18 +46,7 @@ public static function getSubscribedEvents(): array public function track(RequestEvent $requestEvent): void { - $request = $requestEvent->getRequest(); - - if (!$requestEvent->isMasterRequest()) { - return; - } - - if (!$request->attributes->has('_route')) { - return; - } - - $route = $request->attributes->get('_route'); - if ('sylius_shop_checkout_start' !== $route) { + if (!$this->isRequestEligible() || !$this->pixelContext->hasPixels()) { return; } @@ -72,13 +55,27 @@ public function track(RequestEvent $requestEvent): void return; } - if (!$this->pixelContext->hasPixels()) { - return; - } - - $this->generatePixelEvents( + $this->pixelEventsGenerator->generatePixelEvents( $cart, ServerSideEventInterface::EVENT_INITIATE_CHECKOUT ); } + + /** + * Request is eligible when: + * - We are on the 'checkout start' page + */ + protected function isRequestEligible(): bool + { + if (!parent::isRequestEligible()) { + return false; + } + + $request = $this->requestStack->getCurrentRequest(); + if (null === $request) { + return false; + } + + return 'sylius_shop_checkout_start' === $request->attributes->get('_route'); + } } diff --git a/src/EventListener/PurchaseSubscriber.php b/src/EventListener/PurchaseSubscriber.php index e8c97ad..98d27cd 100644 --- a/src/EventListener/PurchaseSubscriber.php +++ b/src/EventListener/PurchaseSubscriber.php @@ -4,11 +4,9 @@ namespace Setono\SyliusFacebookPlugin\EventListener; -use Doctrine\ORM\EntityManagerInterface; +use Setono\BotDetectionBundle\BotDetector\BotDetectorInterface; use Setono\SyliusFacebookPlugin\Context\PixelContextInterface; -use Setono\SyliusFacebookPlugin\DataMapper\DataMapperInterface; -use Setono\SyliusFacebookPlugin\Factory\PixelEventFactoryInterface; -use Setono\SyliusFacebookPlugin\ServerSide\ServerSideEventFactoryInterface; +use Setono\SyliusFacebookPlugin\Generator\PixelEventsGeneratorInterface; use Setono\SyliusFacebookPlugin\ServerSide\ServerSideEventInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Order\Repository\OrderRepositoryInterface; @@ -22,23 +20,19 @@ final class PurchaseSubscriber extends AbstractSubscriber protected OrderRepositoryInterface $orderRepository; public function __construct( - PixelContextInterface $pixelContext, RequestStack $requestStack, FirewallMap $firewallMap, - ServerSideEventFactoryInterface $serverSideFactory, - DataMapperInterface $dataMapper, - PixelEventFactoryInterface $pixelEventFactory, - EntityManagerInterface $entityManager, + PixelContextInterface $pixelContext, + PixelEventsGeneratorInterface $pixelEventsGenerator, + BotDetectorInterface $botDetector, OrderRepositoryInterface $orderRepository ) { parent::__construct( - $pixelContext, $requestStack, $firewallMap, - $serverSideFactory, - $dataMapper, - $pixelEventFactory, - $entityManager + $pixelContext, + $pixelEventsGenerator, + $botDetector ); $this->orderRepository = $orderRepository; @@ -53,41 +47,48 @@ public static function getSubscribedEvents(): array public function track(RequestEvent $requestEvent): void { - $order = $this->resolveOrder($requestEvent); - if (null === $order) { + if (!$this->isRequestEligible() || !$this->pixelContext->hasPixels()) { return; } - if (!$this->pixelContext->hasPixels()) { + $order = $this->resolveOrder(); + if (null === $order) { return; } - $this->generatePixelEvents( + $this->pixelEventsGenerator->generatePixelEvents( $order, ServerSideEventInterface::EVENT_PURCHASE ); } /** - * This method will return an OrderInterface if + * Request is eligible when: * - We are on the 'thank you' page - * - A session exists with the order id - * - The order can be found in the order repository */ - private function resolveOrder(RequestEvent $requestEvent): ?OrderInterface + protected function isRequestEligible(): bool { - $request = $requestEvent->getRequest(); - - if (!$requestEvent->isMasterRequest()) { - return null; + if (!parent::isRequestEligible()) { + return false; } - if (!$request->attributes->has('_route')) { - return null; + $request = $this->requestStack->getCurrentRequest(); + if (null === $request) { + return false; } - $route = $request->attributes->get('_route'); - if ('sylius_shop_order_thank_you' !== $route) { + return 'sylius_shop_order_thank_you' === $request->attributes->get('_route'); + } + + /** + * This method will return an OrderInterface if + * - A session exists with the order id + * - The order can be found in the order repository + */ + private function resolveOrder(): ?OrderInterface + { + $request = $this->requestStack->getCurrentRequest(); + if (null === $request) { return null; } diff --git a/src/EventListener/ViewCategorySubscriber.php b/src/EventListener/ViewCategorySubscriber.php index c3333b8..4399bb8 100644 --- a/src/EventListener/ViewCategorySubscriber.php +++ b/src/EventListener/ViewCategorySubscriber.php @@ -4,12 +4,10 @@ namespace Setono\SyliusFacebookPlugin\EventListener; -use Doctrine\ORM\EntityManagerInterface; +use Setono\BotDetectionBundle\BotDetector\BotDetectorInterface; use Setono\SyliusFacebookPlugin\Context\PixelContextInterface; use Setono\SyliusFacebookPlugin\Data\ViewCategoryData; -use Setono\SyliusFacebookPlugin\DataMapper\DataMapperInterface; -use Setono\SyliusFacebookPlugin\Factory\PixelEventFactoryInterface; -use Setono\SyliusFacebookPlugin\ServerSide\ServerSideEventFactoryInterface; +use Setono\SyliusFacebookPlugin\Generator\PixelEventsGeneratorInterface; use Setono\SyliusFacebookPlugin\ServerSide\ServerSideEventInterface; use Sylius\Bundle\ResourceBundle\Event\ResourceControllerEvent; use Sylius\Bundle\ResourceBundle\Grid\View\ResourceGridView; @@ -32,24 +30,20 @@ final class ViewCategorySubscriber extends AbstractSubscriber protected TaxonRepositoryInterface $taxonRepository; public function __construct( - PixelContextInterface $pixelContext, RequestStack $requestStack, FirewallMap $firewallMap, - ServerSideEventFactoryInterface $serverSideFactory, - DataMapperInterface $dataMapper, - PixelEventFactoryInterface $pixelEventFactory, - EntityManagerInterface $entityManager, + PixelContextInterface $pixelContext, + PixelEventsGeneratorInterface $pixelEventsGenerator, + BotDetectorInterface $botDetector, LocaleContextInterface $localeContext, TaxonRepositoryInterface $taxonRepository ) { parent::__construct( - $pixelContext, $requestStack, $firewallMap, - $serverSideFactory, - $dataMapper, - $pixelEventFactory, - $entityManager + $pixelContext, + $pixelEventsGenerator, + $botDetector ); $this->localeContext = $localeContext; @@ -67,7 +61,7 @@ public static function getSubscribedEvents(): array public function trackCustom(ResourceControllerEvent $event): void { - if (!$this->isShopContext() || !$this->pixelContext->hasPixels()) { + if (!$this->isRequestEligible() || !$this->pixelContext->hasPixels()) { return; } @@ -81,7 +75,7 @@ public function trackCustom(ResourceControllerEvent $event): void $this->getTaxon($gridView), ); - $this->generatePixelEvents( + $this->pixelEventsGenerator->generatePixelEvents( $viewCategoryData, ServerSideEventInterface::CUSTOM_EVENT_VIEW_CATEGORY ); diff --git a/src/EventListener/ViewContentSubscriber.php b/src/EventListener/ViewContentSubscriber.php index 0e4d369..61a6ba9 100644 --- a/src/EventListener/ViewContentSubscriber.php +++ b/src/EventListener/ViewContentSubscriber.php @@ -21,7 +21,7 @@ public static function getSubscribedEvents(): array public function track(ResourceControllerEvent $event): void { - if (!$this->isShopContext() || !$this->pixelContext->hasPixels()) { + if (!$this->isRequestEligible() || !$this->pixelContext->hasPixels()) { return; } @@ -30,7 +30,7 @@ public function track(ResourceControllerEvent $event): void return; } - $this->generatePixelEvents( + $this->pixelEventsGenerator->generatePixelEvents( $product, ServerSideEventInterface::EVENT_VIEW_CONTENT ); diff --git a/src/Generator/PixelEventsGenerator.php b/src/Generator/PixelEventsGenerator.php new file mode 100644 index 0000000..e497087 --- /dev/null +++ b/src/Generator/PixelEventsGenerator.php @@ -0,0 +1,67 @@ +pixelContext = $pixelContext; + $this->requestStack = $requestStack; + $this->serverSideFactory = $serverSideFactory; + $this->dataMapper = $dataMapper; + $this->pixelEventFactory = $pixelEventFactory; + $this->entityManager = $entityManager; + } + + /** + * @param object $source + */ + public function generatePixelEvents($source, string $eventName, Request $request = null): void + { + $serverSideEvent = $this->serverSideFactory->create($eventName); + $this->dataMapper->map($source, $serverSideEvent, [ + 'request' => $request ?? $this->requestStack->getMasterRequest(), + 'event' => $eventName, + ]); + + $pixels = $this->pixelContext->getPixels(); + foreach ($pixels as $pixel) { + // @todo Maybe its better to just clone + $pixelEvent = $this->pixelEventFactory->createFromServerSideEvent($serverSideEvent); + $pixelEvent->setPixel($pixel); + + $this->entityManager->persist($pixelEvent); + } + + $this->entityManager->flush(); + } +} diff --git a/src/Generator/PixelEventsGeneratorInterface.php b/src/Generator/PixelEventsGeneratorInterface.php new file mode 100644 index 0000000..867bc60 --- /dev/null +++ b/src/Generator/PixelEventsGeneratorInterface.php @@ -0,0 +1,15 @@ + + diff --git a/src/Resources/config/services/event_listener.xml b/src/Resources/config/services/event_listener.xml index c835405..0c7292b 100644 --- a/src/Resources/config/services/event_listener.xml +++ b/src/Resources/config/services/event_listener.xml @@ -6,13 +6,11 @@ - - - - - + + + + + + + + + + + + + + + + + diff --git a/tests/Application/config/bundles.php b/tests/Application/config/bundles.php index aeecb07..0630a0e 100644 --- a/tests/Application/config/bundles.php +++ b/tests/Application/config/bundles.php @@ -54,5 +54,6 @@ Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true, 'test_cached' => true], Setono\ClientIdBundle\SetonoClientIdBundle::class => ['all' => true], Setono\ConsentBundle\SetonoConsentBundle::class => ['all' => true], + Setono\BotDetectionBundle\SetonoBotDetectionBundle::class => ['all' => true], FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle::class => ['test' => true, 'test_cached' => true], ];