diff --git a/composer.json b/composer.json index 3db88aa6..0fed51fb 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "spryker/checkout-extension": "^1.2.0", "spryker/country": "^3.1.0 || ^4.0.0", "spryker/customer-extension": "^1.5.0", + "spryker/event-dispatcher-extension": "^1.0.0", "spryker/gui": "^3.39.0", "spryker/kernel": "^3.72.0", "spryker/locale": "^3.0.0 || ^4.0.0", @@ -47,6 +48,8 @@ }, "suggest": { "spryker/checkout": "If you want to use Checkout plugins.", + "spryker/container": "To use EventDispatcher plugins.", + "spryker/event-dispatcher": "To use EventDispatcher plugins.", "spryker/log": "If you want to use Log plugins.", "spryker/sales": "If you want customer information in sales." }, diff --git a/src/Spryker/Client/Customer/CustomerClient.php b/src/Spryker/Client/Customer/CustomerClient.php index 1a5f8330..f2a98704 100644 --- a/src/Spryker/Client/Customer/CustomerClient.php +++ b/src/Spryker/Client/Customer/CustomerClient.php @@ -577,4 +577,16 @@ public function updateCustomerAddresses(CustomerTransfer $customerTransfer): voi ->createCustomerAddressUpdater() ->updateCustomerAddresses($customerTransfer); } + + /** + * {@inheritDoc} + * + * @api + * + * @return string + */ + public function getUserIdentifier(): string + { + return $this->getFactory()->createSessionCustomerSession()->getUserIdentifier(); + } } diff --git a/src/Spryker/Client/Customer/CustomerClientInterface.php b/src/Spryker/Client/Customer/CustomerClientInterface.php index 37562d3b..e852df2f 100644 --- a/src/Spryker/Client/Customer/CustomerClientInterface.php +++ b/src/Spryker/Client/Customer/CustomerClientInterface.php @@ -447,4 +447,15 @@ public function getCustomerByAccessToken(string $accessToken): CustomerResponseT * @return void */ public function updateCustomerAddresses(CustomerTransfer $customerTransfer): void; + + /** + * Specification: + * - Returns logged-in customer identifier. + * - Otherwise, returns an identifier for anonymous from the session. + * + * @api + * + * @return string + */ + public function getUserIdentifier(): string; } diff --git a/src/Spryker/Client/Customer/CustomerFactory.php b/src/Spryker/Client/Customer/CustomerFactory.php index 95d9d460..67bbee36 100644 --- a/src/Spryker/Client/Customer/CustomerFactory.php +++ b/src/Spryker/Client/Customer/CustomerFactory.php @@ -118,7 +118,7 @@ public function getCustomerSecuredPatternRulePlugins(): array /** * @return \Spryker\Client\Session\SessionClientInterface */ - protected function getSessionClient() + public function getSessionClient() { return $this->getProvidedDependency(CustomerDependencyProvider::SERVICE_SESSION); } diff --git a/src/Spryker/Client/Customer/Session/CustomerSession.php b/src/Spryker/Client/Customer/Session/CustomerSession.php index 25faae8d..07c051a3 100644 --- a/src/Spryker/Client/Customer/Session/CustomerSession.php +++ b/src/Spryker/Client/Customer/Session/CustomerSession.php @@ -10,6 +10,7 @@ use Generated\Shared\Transfer\CustomerTransfer; use Spryker\Client\Customer\Exception\EmptyCustomerTransferCacheException; use Spryker\Client\Session\SessionClientInterface; +use Spryker\Shared\Customer\CustomerConfig; class CustomerSession implements CustomerSessionInterface { @@ -196,4 +197,14 @@ protected function invalidateCustomerTransferCache(): void { static::$customerTransferCache = null; } + + /** + * @return string + */ + public function getUserIdentifier(): string + { + $customerTransfer = $this->getCustomer(); + + return $customerTransfer ? $customerTransfer->getCustomerReference() : $this->sessionClient->get(CustomerConfig::ANONYMOUS_SESSION_KEY, ''); + } } diff --git a/src/Spryker/Client/Customer/Session/CustomerSessionInterface.php b/src/Spryker/Client/Customer/Session/CustomerSessionInterface.php index 0b5af871..4daedad8 100644 --- a/src/Spryker/Client/Customer/Session/CustomerSessionInterface.php +++ b/src/Spryker/Client/Customer/Session/CustomerSessionInterface.php @@ -49,4 +49,9 @@ public function findCustomerRawData(): ?CustomerTransfer; * @return void */ public function markCustomerAsDirty(); + + /** + * @return string + */ + public function getUserIdentifier(): string; } diff --git a/src/Spryker/Shared/Customer/CustomerConfig.php b/src/Spryker/Shared/Customer/CustomerConfig.php index 6a6ce6ff..87a117fd 100644 --- a/src/Spryker/Shared/Customer/CustomerConfig.php +++ b/src/Spryker/Shared/Customer/CustomerConfig.php @@ -11,6 +11,11 @@ class CustomerConfig extends AbstractSharedConfig { + /** + * @var string + */ + public const ANONYMOUS_SESSION_KEY = 'anonymousID'; + /** * @api * diff --git a/src/Spryker/Yves/Customer/CustomerDependencyProvider.php b/src/Spryker/Yves/Customer/CustomerDependencyProvider.php index 358e18f1..96a2b147 100644 --- a/src/Spryker/Yves/Customer/CustomerDependencyProvider.php +++ b/src/Spryker/Yves/Customer/CustomerDependencyProvider.php @@ -22,6 +22,11 @@ class CustomerDependencyProvider extends AbstractBundleDependencyProvider */ public const SERVICE_REQUEST_STACK = 'request_stack'; + /** + * @var string + */ + public const SERVICE_UTIL_TEXT = 'SERVICE_UTIL_TEXT'; + /** * @param \Spryker\Yves\Kernel\Container $container * @@ -30,6 +35,7 @@ class CustomerDependencyProvider extends AbstractBundleDependencyProvider public function provideDependencies(Container $container): Container { $container = $this->addRequestStackService($container); + $container = $this->addUtilTextService($container); return $container; } @@ -47,4 +53,18 @@ protected function addRequestStackService(Container $container): Container return $container; } + + /** + * @param \Spryker\Yves\Kernel\Container $container + * + * @return \Spryker\Yves\Kernel\Container + */ + protected function addUtilTextService(Container $container) + { + $container->set(static::SERVICE_UTIL_TEXT, function (Container $container) { + return $container->getLocator()->utilText()->service(); + }); + + return $container; + } } diff --git a/src/Spryker/Yves/Customer/CustomerFactory.php b/src/Spryker/Yves/Customer/CustomerFactory.php index 4030a20f..916ffd82 100644 --- a/src/Spryker/Yves/Customer/CustomerFactory.php +++ b/src/Spryker/Yves/Customer/CustomerFactory.php @@ -7,8 +7,11 @@ namespace Spryker\Yves\Customer; +use Spryker\Service\UtilText\UtilTextServiceInterface; use Spryker\Yves\Customer\Processor\CurrentCustomerDataRequestLogProcessor; use Spryker\Yves\Customer\Processor\CurrentCustomerDataRequestLogProcessorInterface; +use Spryker\Yves\Customer\Session\AnonymousIdProvider; +use Spryker\Yves\Customer\Session\AnonymousIdProviderInterface; use Spryker\Yves\Kernel\AbstractFactory; use Symfony\Component\HttpFoundation\RequestStack; @@ -29,4 +32,20 @@ public function getRequestStackService(): RequestStack { return $this->getProvidedDependency(CustomerDependencyProvider::SERVICE_REQUEST_STACK); } + + /** + * @return \Spryker\Yves\Customer\Session\AnonymousIdProviderInterface + */ + public function createAnonymousIdProvider(): AnonymousIdProviderInterface + { + return new AnonymousIdProvider($this->getUtilTextService()); + } + + /** + * @return \Spryker\Service\UtilText\UtilTextServiceInterface + */ + public function getUtilTextService(): UtilTextServiceInterface + { + return $this->getProvidedDependency(CustomerDependencyProvider::SERVICE_UTIL_TEXT); + } } diff --git a/src/Spryker/Yves/Customer/Plugin/EventDispatcher/AnonymousIdSessionAssignEventDispatcherPlugin.php b/src/Spryker/Yves/Customer/Plugin/EventDispatcher/AnonymousIdSessionAssignEventDispatcherPlugin.php new file mode 100644 index 00000000..63520c0b --- /dev/null +++ b/src/Spryker/Yves/Customer/Plugin/EventDispatcher/AnonymousIdSessionAssignEventDispatcherPlugin.php @@ -0,0 +1,52 @@ +addListener(KernelEvents::REQUEST, function (RequestEvent $event) { + if (!$event->isMainRequest()) { + return; + } + + $request = $event->getRequest(); + $session = $request->getSession(); + + $anonymousId = $session->get(CustomerConfig::ANONYMOUS_SESSION_KEY); + if ($anonymousId === null) { + $session->set(CustomerConfig::ANONYMOUS_SESSION_KEY, $this->getFactory()->createAnonymousIdProvider()->generateUniqueId()); + } + }, static::LISTENER_PRIORITY); + + return $eventDispatcher; + } +} diff --git a/src/Spryker/Yves/Customer/Session/AnonymousIdProvider.php b/src/Spryker/Yves/Customer/Session/AnonymousIdProvider.php new file mode 100644 index 00000000..2ea8b98c --- /dev/null +++ b/src/Spryker/Yves/Customer/Session/AnonymousIdProvider.php @@ -0,0 +1,31 @@ +utilTextService = $utilTextService; + } + + /** + * @return string + */ + public function generateUniqueId(): string + { + return 'anonymous-' . $this->utilTextService->generateRandomString(16); + } +} diff --git a/src/Spryker/Yves/Customer/Session/AnonymousIdProviderInterface.php b/src/Spryker/Yves/Customer/Session/AnonymousIdProviderInterface.php new file mode 100644 index 00000000..8c5d0de1 --- /dev/null +++ b/src/Spryker/Yves/Customer/Session/AnonymousIdProviderInterface.php @@ -0,0 +1,16 @@ +tester->mockFactoryMethod( + 'createSessionCustomerSession', + new CustomerSession( + $this->tester->getSessionClientMock([CustomerConfig::ANONYMOUS_SESSION_KEY => 'anonymous:123']), + ), + ); + + $customerClient = new CustomerClient(); + $customerClient->setFactory($this->tester->getFactory()); + + // Act + $userIdentifier = $customerClient->getUserIdentifier(); + + // Assert + $this->assertSame('anonymous:123', $userIdentifier); + } + + /** + * @return void + */ + public function testGetUserIdentifierReturnsTheCustomerReferenceFromTheSession(): void + { + // Arrange + $this->tester->mockFactoryMethod( + 'createSessionCustomerSession', + new CustomerSession( + $this->tester->getSessionClientMock([CustomerSession::SESSION_KEY => (new CustomerTransfer())->setCustomerReference('registered:123')]), + ), + ); + + $customerClient = new CustomerClient(); + $customerClient->setFactory($this->tester->getFactory()); + + // Act + $userIdentifier = $customerClient->getUserIdentifier(); + + // Assert + $this->assertSame('registered:123', $userIdentifier); + } +} diff --git a/tests/SprykerTest/Client/Customer/_support/CustomerClientTester.php b/tests/SprykerTest/Client/Customer/_support/CustomerClientTester.php index 7cbfd304..3a24ba5c 100644 --- a/tests/SprykerTest/Client/Customer/_support/CustomerClientTester.php +++ b/tests/SprykerTest/Client/Customer/_support/CustomerClientTester.php @@ -8,6 +8,8 @@ namespace SprykerTest\Client\Customer; use Codeception\Actor; +use Codeception\Stub; +use Spryker\Client\Session\SessionClientInterface; /** * Inherited Methods @@ -30,6 +32,16 @@ class CustomerClientTester extends Actor use _generated\CustomerClientTesterActions; /** - * Define custom actions here + * @param array $returnedValues + * + * @return \Spryker\Client\Session\SessionClientInterface */ + public function getSessionClientMock(array $returnedValues = []): SessionClientInterface + { + return Stub::makeEmpty(SessionClientInterface::class, [ + 'get' => function ($key) use (&$returnedValues) { + return $returnedValues[$key] ?? null; + }, + ]); + } } diff --git a/tests/SprykerTest/Client/Customer/codeception.yml b/tests/SprykerTest/Client/Customer/codeception.yml index 30f8c2cf..3d97afe0 100644 --- a/tests/SprykerTest/Client/Customer/codeception.yml +++ b/tests/SprykerTest/Client/Customer/codeception.yml @@ -20,3 +20,4 @@ suites: - \SprykerTest\Shared\Testify\Helper\Environment - \SprykerTest\Shared\Testify\Helper\ConfigHelper - \SprykerTest\Shared\Testify\Helper\LocatorHelper + - \SprykerTest\Client\Testify\Helper\FactoryHelper diff --git a/tests/SprykerTest/Yves/Customer/Plugin/EventDispatcher/AnonymousIdSessionAssignEventDispatcherPluginTest.php b/tests/SprykerTest/Yves/Customer/Plugin/EventDispatcher/AnonymousIdSessionAssignEventDispatcherPluginTest.php new file mode 100644 index 00000000..ae8bb009 --- /dev/null +++ b/tests/SprykerTest/Yves/Customer/Plugin/EventDispatcher/AnonymousIdSessionAssignEventDispatcherPluginTest.php @@ -0,0 +1,84 @@ +dispatchRequestEvent($plugin, [CustomerConfig::ANONYMOUS_SESSION_KEY => null]); + + // Assert + $request = $event->getRequest(); + $this->assertNotEmpty($request->getSession()->get(CustomerConfig::ANONYMOUS_SESSION_KEY)); + } + + /** + * @return void + */ + public function testThePluginDoesNotSetAnonymousIdToSessionWhenItIsAlreadyInSession(): void + { + // Arrange + $plugin = new AnonymousIdSessionAssignEventDispatcherPlugin(); + + // Act + $event = $this->dispatchRequestEvent($plugin, [CustomerConfig::ANONYMOUS_SESSION_KEY => '123']); + + // Assert + $request = $event->getRequest(); + $this->assertSame('123', $request->getSession()->get(CustomerConfig::ANONYMOUS_SESSION_KEY), 'The anonymous id should not be changed.'); + } + + /** + * @param \Spryker\Shared\EventDispatcherExtension\Dependency\Plugin\EventDispatcherPluginInterface $plugin + * @param array $sessionSeed + * + * @return \Symfony\Component\HttpKernel\Event\RequestEvent + */ + protected function dispatchRequestEvent(EventDispatcherPluginInterface $plugin, array $sessionSeed = []): RequestEvent + { + $eventDispatcher = new EventDispatcher(); + $plugin->extend($eventDispatcher, $this->tester->getContainer()); + + /** @var \Symfony\Component\HttpKernel\Event\RequestEvent $event */ + $event = $eventDispatcher->dispatch($this->tester->getRequestEvent($sessionSeed), KernelEvents::REQUEST); + + return $event; + } +} diff --git a/tests/SprykerTest/Yves/Customer/_support/CustomerTester.php b/tests/SprykerTest/Yves/Customer/_support/CustomerTester.php index fa701e30..fad6771e 100644 --- a/tests/SprykerTest/Yves/Customer/_support/CustomerTester.php +++ b/tests/SprykerTest/Yves/Customer/_support/CustomerTester.php @@ -8,6 +8,11 @@ namespace SprykerTest\Yves\Customer; use Codeception\Actor; +use Codeception\Stub; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Inherited Methods @@ -28,4 +33,46 @@ class CustomerTester extends Actor { use _generated\CustomerTesterActions; + + /** + * @param array $sessionSeed + * + * @return \Symfony\Component\HttpKernel\Event\RequestEvent + */ + public function getRequestEvent(array $sessionSeed = []): RequestEvent + { + $request = Request::createFromGlobals(); + + $request->setSession($this->getHttpSessionMock($sessionSeed)); + + return new RequestEvent($this->getHttpKernelMock(), $request, HttpKernelInterface::MAIN_REQUEST); + } + + /** + * @return \Symfony\Component\HttpKernel\HttpKernelInterface + */ + protected function getHttpKernelMock(): HttpKernelInterface + { + /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $httpKernelMock */ + $httpKernelMock = Stub::makeEmpty(HttpKernelInterface::class); + + return $httpKernelMock; + } + + /** + * @param array $returnedValues + * + * @return \Symfony\Component\HttpFoundation\Session\SessionInterface + */ + protected function getHttpSessionMock(array $returnedValues = []): SessionInterface + { + return Stub::makeEmpty(SessionInterface::class, [ + 'get' => function ($key) use (&$returnedValues) { + return $returnedValues[$key] ?? null; + }, + 'set' => function ($key, $value) use (&$returnedValues) { + $returnedValues[$key] = $value; + }, + ]); + } } diff --git a/tests/SprykerTest/Yves/Customer/codeception.yml b/tests/SprykerTest/Yves/Customer/codeception.yml index bf460f6f..ada0cb45 100644 --- a/tests/SprykerTest/Yves/Customer/codeception.yml +++ b/tests/SprykerTest/Yves/Customer/codeception.yml @@ -18,3 +18,4 @@ suites: enabled: - \SprykerTest\Shared\Testify\Helper\Environment - \SprykerTest\Shared\Testify\Helper\DependencyHelper + - \SprykerTest\Service\Container\Helper\ContainerHelper