From 44f1c397837021f7aba4ad46d27bc05ab1beb6fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Tesa=CC=81rek?= Date: Fri, 20 Mar 2020 11:01:57 +0100 Subject: [PATCH 1/3] Implementation of boosting rules (#109) for Matej9 --- README.md | 9 +++++++++ src/Model/Command/UserRecommendation.php | 8 ++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 42b51b8..97f97eb 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,15 @@ $recommendations = $response->getRecommendation()->getData(); // } ``` +You can further modify which items will be reccomended by providing boosting rules. Priority of items matching the +MQL `$criteria` will be multiplied by the value of `multiplier`: + +```php +$reccomendation = UserRecommendation::create('user-id', 5, 'test-scenario', 1.0, 3600) + ->addBoost(Boost::create('valid_to >= NOW()', 2)) + ->addBoost(Boost::create('for_recommendation = 1', 3.5)) +``` + #### Recommendation response properties Every item in Matej has its id, and optionally other item properties. These properties can be set up in [item properties setup](#item-properties-setup-to-setup-you-matej-database), diff --git a/src/Model/Command/UserRecommendation.php b/src/Model/Command/UserRecommendation.php index df39f1f..e561621 100644 --- a/src/Model/Command/UserRecommendation.php +++ b/src/Model/Command/UserRecommendation.php @@ -268,10 +268,10 @@ protected function getCommandType(): string protected function getSerializedBoosts(): array { return array_map( - function (Boost $boost) { - return $boost->jsonSerialize(); - }, - $this->boosts + function (Boost $boost) { + return $boost->jsonSerialize(); + }, + $this->boosts ); } From 8daa432ce95ce8c3f76b4d1995319ca68afc34d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Tesa=CC=81rek?= Date: Sun, 22 Mar 2020 21:36:25 +0100 Subject: [PATCH 2/3] UserReccomendation requires only user_id and scenario arguments (#108) --- README.md | 35 +++++--- UPGRADE-3.0.md | 32 +++++++ src/Model/Command/UserRecommendation.php | 88 +++++++++++-------- .../CampaignRequestBuilderTest.php | 11 +-- .../RecommendationRequestBuilderTest.php | 11 +-- .../Model/Command/UserRecommendationTest.php | 25 +++--- .../CampaignRequestBuilderTest.php | 32 +++++-- .../RecommendationRequestBuilderTest.php | 30 +++++-- .../RequestBuilderFactoryTest.php | 5 +- 9 files changed, 182 insertions(+), 87 deletions(-) create mode 100644 UPGRADE-3.0.md diff --git a/README.md b/README.md index 97f97eb..8ca70c5 100644 --- a/README.md +++ b/README.md @@ -182,7 +182,7 @@ when providing recommendations. $matej = new Matej('accountId', 'apikey'); $response = $matej->request() - ->recommendation(UserRecommendation::create('user-id', 5, 'test-scenario', 1.0, 3600)) + ->recommendation(UserRecommendation::create('user-id', 'test-scenario')) ->setInteraction(Interaction::purchase('user-id', 'item-id')) // optional ->setUserMerge(UserMerge::mergeInto('user-id', 'source-id')) // optional ->send(); @@ -193,9 +193,12 @@ $recommendations = $response->getRecommendation()->getData(); You can also set more granular options of the recommendation command: ```php -$recommendation = UserRecommendation::create('user-id', 5, 'test-scenario', 1.0, 3600); -$recommendation->setFilters(['for_recommendation = 1']) +$recommendation = UserRecommendation::create('user-id', 'test-scenario') + ->setCount(5) + ->setRotationRate(1.0) + ->setRotationTime(3600) + ->setFilters(['for_recommendation = 1']) ->setMinimalRelevance(MinimalRelevance::HIGH()) ->enableHardRotation() ->addBoost(Boost::create('valid_to >= NOW()', 2)); @@ -233,9 +236,9 @@ You can further modify which items will be reccomended by providing boosting rul MQL `$criteria` will be multiplied by the value of `multiplier`: ```php -$reccomendation = UserRecommendation::create('user-id', 5, 'test-scenario', 1.0, 3600) +$reccomendation = UserRecommendation::create('user-id', 'test-scenario') ->addBoost(Boost::create('valid_to >= NOW()', 2)) - ->addBoost(Boost::create('for_recommendation = 1', 3.5)) + ->addBoost(Boost::create('for_recommendation = 1', 3.5)); ``` #### Recommendation response properties @@ -244,12 +247,11 @@ Every item in Matej has its id, and optionally other item properties. These prop and you can upload item data in the [events](#send-events-data-to-matej) request. This has major benefit because you can request these properties to be returned as part of your Recommendation Request. -We call them response properties, and they can be specified either as the last parameter of `UserRecommendation::create` function, -by calling `->addResponseProperty()` method, or by calling `->setResponseProperties()` method. Following will request an `item_id`, +We call them response properties. They can be specified by calling `->addResponseProperty()` method or by calling `->setResponseProperties()` method. Following will request an `item_id`, `item_url` and `item_title`: ```php -$recommendation = UserRecommendation::create('user-id', 5, 'test-scenario', 1.0, 3600, ['item_url']) +$recommendation = UserRecommendation::create('user-id', 'test-scenario') ->addResponseProperty('item_title'); $response = $matej->request() @@ -329,8 +331,12 @@ $response = $matej->request() ->addSorting(Sorting::create('user-id', ['item-id-1', 'item-id-2', 'item-id-3'])) ->addSortings([/* array of Sorting objects */]) // Request user-based recommendations - ->addRecommendation(UserRecommendation::create('user-id', 10, 'emailing', 1.0, 3600)) - ->addRecommendations([/* array of UserRecommendation objects */]) + ->addRecommendation( + UserRecommendation::create('user-id', 'emailing') + ->setCount(10) + ->setRotationRate(1.0) + ->setRotationTime(3600) + )->addRecommendations([/* array of UserRecommendation objects */]) ->send(); ``` @@ -341,8 +347,11 @@ but once available, you can specify which model you want to use when requesting This is available for `recommendation`, `sorting` and `campaign` requests: ```php -$recommendationCommand = UserRecommendation::create('user-id', 5, 'test-scenario', 1.0, 3600); -$recommendationCommand->setModelName('alpha'); +$recommendationCommand = UserRecommendation::create('user-id', 'test-scenario'); + ->setCount(5) + ->setRotationRate(1.0) + ->setRotationTime(3600) + ->setModelName('alpha'); $sortingCommand = Sorting::create('user-id', ['item-id-1', 'item-id-2', 'item-id-3']); $sortingCommand->setModelName('beta') @@ -368,7 +377,7 @@ Typically, you'd select a random sample of users, to which you'd present recomme in your code should look similar to this: ```php -$recommendation = UserRecommendation::create('user-id', 5, 'test-scenario', 1.0, 3600); +$recommendation = UserRecommendation::create('user-id', 'test-scenario') if ($session->isUserInBucketB()) { $recommendation->setModelName('alpha'); diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md new file mode 100644 index 0000000..6e5da14 --- /dev/null +++ b/UPGRADE-3.0.md @@ -0,0 +1,32 @@ +# Upgrading from 2.x to 3.0 + +API client release 2.0 contains few backward incompatible changes. + +This guide will help you upgrade your codebase. + +## `UserRecommendation::create()` now accepts only `$user_id` and `$scenario` +`UserReccomentation::create()` accepts only two argumens: `$user_id` and `$scenario`. +Both are arguments are required. Reccomendation command can be further parametrized +using fluent API. + +#### Before +```php +$recommendation = UserRecommendation::create('user-id', 5, 'scenario', 1.0, 3600); +``` + +#### After +```php +$recommendation = UserRecommendation::create('user-id', 'scenario') + ->setCount(5) + ->setRotationRate(1.0) + ->setRotationTime(3600); +``` + +which is equivalent to + +```php +$recommendation = UserRecommendation::create('user-id', 'scenario'); +$recommendation->setCount(5); +$recommendation->setRotationRate(1.0); +$recommendation->setRotationTime(3600); +``` \ No newline at end of file diff --git a/src/Model/Command/UserRecommendation.php b/src/Model/Command/UserRecommendation.php index e561621..a4e2c0c 100644 --- a/src/Model/Command/UserRecommendation.php +++ b/src/Model/Command/UserRecommendation.php @@ -41,46 +41,22 @@ class UserRecommendation extends AbstractCommand implements UserAwareInterface /** @var Boost[] */ private $boosts = []; - private function __construct( - string $userId, - int $count, - string $scenario, - float $rotationRate, - int $rotationTime, - array $responseProperties - ) { + private function __construct(string $userId, string $scenario) + { $this->minimalRelevance = MinimalRelevance::LOW(); - $this->setUserId($userId); - $this->setCount($count); $this->setScenario($scenario); - $this->setRotationRate($rotationRate); - $this->setRotationTime($rotationTime); - $this->setResponseProperties($responseProperties); } /** * @param string $userId - * @param int $count Number of requested recommendations. The real number of recommended items could be lower or - * even zero when there are no items relevant for the user. * @param string $scenario Name of the place where recommendations are applied - eg. 'search-results-page', * 'emailing', 'empty-search-results, 'homepage', ... - * @param float $rotationRate How much should the item be penalized for being recommended again in the near future. - * Set from 0.0 for no rotation (same items will be recommended) up to 1.0 (same items should not be recommended). - * @param int $rotationTime Specify for how long will the item's rotationRate be taken in account and so the item - * is penalized for recommendations. - * @param string[] $responseProperties Specify which properties you want to retrieve from Matej alongside the item_id. * @return static */ - public static function create( - string $userId, - int $count, - string $scenario, - float $rotationRate, - int $rotationTime, - array $responseProperties = [] - ): self { - return new static($userId, $count, $scenario, $rotationRate, $rotationTime, $responseProperties); + public static function create(string $userId, string $scenario): self + { + return new static($userId, $scenario); } /** @@ -137,7 +113,11 @@ public function setFilters(array $filters): self } /** - * Add another response property you want returned. item_id is always returned by Matej. + * Add another response property you want returned. item_id is always + * returned by Matej. + * + * @param string $property + * @return $this */ public function addResponseProperty(string $property): self { @@ -227,11 +207,20 @@ protected function setUserId(string $userId): void $this->userId = $userId; } - protected function setCount(int $count): void + /** + * Set number of requested recommendations. The real number of recommended + * items could be lower or even zero when there are no items relevant for + * the user. + * + * @return $this + */ + public function setCount(int $count): self { Assertion::greaterThan($count, 0); $this->count = $count; + + return $this; } protected function setScenario(string $scenario): void @@ -241,18 +230,36 @@ protected function setScenario(string $scenario): void $this->scenario = $scenario; } - protected function setRotationRate(float $rotationRate): void + /** + * Set how much should the item be penalized for being recommended again in + * the near future. + * + * @param float $rotationRate + * @return $this + */ + public function setRotationRate(float $rotationRate): self { Assertion::between($rotationRate, 0, 1); $this->rotationRate = $rotationRate; + + return $this; } - protected function setRotationTime(int $rotationTime): void + /** + * Specify for how long will the item's rotationRate be taken in account and + * so the item is penalized for recommendations. + * + * @param int $rotationTime + * @return $this + */ + public function setRotationTime(int $rotationTime): self { Assertion::greaterOrEqualThan($rotationTime, 0); $this->rotationTime = $rotationTime; + + return $this; } protected function assembleFiltersString(): string @@ -279,10 +286,7 @@ protected function getCommandParameters(): array { $parameters = [ 'user_id' => $this->userId, - 'count' => $this->count, 'scenario' => $this->scenario, - 'rotation_rate' => $this->rotationRate, - 'rotation_time' => $this->rotationTime, 'hard_rotation' => $this->hardRotation, 'min_relevance' => $this->minimalRelevance->jsonSerialize(), 'filter' => $this->assembleFiltersString(), @@ -290,6 +294,18 @@ protected function getCommandParameters(): array 'properties' => $this->responseProperties, ]; + if ($this->count !== null) { + $parameters['count'] = $this->count; + } + + if ($this->rotationRate !== null) { + $parameters['rotation_rate'] = $this->rotationRate; + } + + if ($this->rotationRate !== null) { + $parameters['rotation_time'] = $this->rotationTime; + } + if ($this->modelName !== null) { $parameters['model_name'] = $this->modelName; } diff --git a/tests/integration/RequestBuilder/CampaignRequestBuilderTest.php b/tests/integration/RequestBuilder/CampaignRequestBuilderTest.php index 196dcef..05a1df8 100644 --- a/tests/integration/RequestBuilder/CampaignRequestBuilderTest.php +++ b/tests/integration/RequestBuilder/CampaignRequestBuilderTest.php @@ -47,13 +47,10 @@ public function shouldExecuteRecommendationAndSortingCommands(): void private function createRecommendationCommand(string $letter): UserRecommendation { - return UserRecommendation::create( - 'user-' . $letter, - 1, - 'integration-test-scenario', - 1, - 3600 - ); + return UserRecommendation::create('user-' . $letter, 'integration-test-scenario') + ->setCount(1) + ->setRotationRate(1) + ->setRotationTime(3600); } private function createSortingCommand(string $letter): Sorting diff --git a/tests/integration/RequestBuilder/RecommendationRequestBuilderTest.php b/tests/integration/RequestBuilder/RecommendationRequestBuilderTest.php index 4b3d5a2..feec868 100644 --- a/tests/integration/RequestBuilder/RecommendationRequestBuilderTest.php +++ b/tests/integration/RequestBuilder/RecommendationRequestBuilderTest.php @@ -79,13 +79,10 @@ public function shouldReturnInvalidCommandOnInvalidPropertyName(): void private function createRecommendationCommand(string $username): UserRecommendation { - return UserRecommendation::create( - $username, - 5, - 'integration-test-scenario', - 0.50, - 3600 - ); + return UserRecommendation::create($username, 'integration-test-scenario') + ->setCount(5) + ->setRotationRate(0.50) + ->setRotationTime(3600); } private function assertShorthandResponse( diff --git a/tests/unit/Model/Command/UserRecommendationTest.php b/tests/unit/Model/Command/UserRecommendationTest.php index 6a7ef05..713c25b 100644 --- a/tests/unit/Model/Command/UserRecommendationTest.php +++ b/tests/unit/Model/Command/UserRecommendationTest.php @@ -10,18 +10,15 @@ class UserRecommendationTest extends TestCase /** @test */ public function shouldBeInstantiableViaNamedConstructorWithDefaultValues(): void { - $command = UserRecommendation::create('user-id', 333, 'test-scenario', 1.0, 3600); + $command = UserRecommendation::create('user-id', 'test-scenario'); $this->assertInstanceOf(UserRecommendation::class, $command); - $this->assertSame( + $this->assertEquals( [ 'type' => 'user-based-recommendations', 'parameters' => [ 'user_id' => 'user-id', - 'count' => 333, 'scenario' => 'test-scenario', - 'rotation_rate' => 1.0, - 'rotation_time' => 3600, 'hard_rotation' => false, 'min_relevance' => MinimalRelevance::LOW, 'filter' => '', @@ -46,7 +43,10 @@ public function shouldUseCustomParameters(): void $rotationTime = random_int(1, 86400); $modelName = 'test-model-' . md5(microtime()); - $command = UserRecommendation::create($userId, $count, $scenario, $rotationRate, $rotationTime); + $command = UserRecommendation::create($userId, $scenario) + ->setCount($count) + ->setRotationRate($rotationRate) + ->setRotationTime($rotationTime); $command->setMinimalRelevance(MinimalRelevance::HIGH()) ->enableHardRotation() @@ -57,7 +57,8 @@ public function shouldUseCustomParameters(): void ->addBoost(Boost::create('custom = argument', 2.0)); $this->assertInstanceOf(UserRecommendation::class, $command); - $this->assertSame( + + $this->assertEquals( [ 'type' => 'user-based-recommendations', 'parameters' => [ @@ -86,7 +87,7 @@ public function shouldUseCustomParameters(): void /** @test */ public function shouldAssembleMqlFilters(): void { - $command = UserRecommendation::create('user-id', 333, 'test-scenario', 1.0, 3600); + $command = UserRecommendation::create('user-id', 'test-scenario'); // Default filter $this->assertSame('', $command->jsonSerialize()['parameters']['filter']); @@ -118,12 +119,12 @@ public function shouldAssembleMqlFilters(): void /** @test */ public function shouldAllowModificationOfResponseProperties(): void { - $command = UserRecommendation::create('user-id', 333, 'test-scenario', 1.0, 3600, ['test']); + $command = UserRecommendation::create('user-id', 'test-scenario'); + $command->addResponseProperty('test'); $this->assertSame(['test'], $command->jsonSerialize()['parameters']['properties']); // Add some properties $command->addResponseProperty('url'); - $this->assertSame(['test', 'url'], $command->jsonSerialize()['parameters']['properties']); // Overwrite all properties @@ -134,7 +135,7 @@ public function shouldAllowModificationOfResponseProperties(): void /** @test */ public function shouldResetBoostRules(): void { - $command = UserRecommendation::create('user-id', 333, 'test-scenario', 1.0, 3600) + $command = UserRecommendation::create('user-id', 'test-scenario') ->addBoost(Boost::create('valid_to >= NOW()', 1.0)); $command->setBoosts([ @@ -154,7 +155,7 @@ public function shouldResetBoostRules(): void /** @test */ public function shouldNotIncludeEmptyBoosts(): void { - $command = UserRecommendation::create('user-id', 333, 'test-scenario', 1.0, 3600) + $command = UserRecommendation::create('user-id', 'test-scenario') ->setBoosts([]); $this->assertArrayNotHasKey('boost_rules', $command->jsonSerialize()['parameters']); diff --git a/tests/unit/RequestBuilder/CampaignRequestBuilderTest.php b/tests/unit/RequestBuilder/CampaignRequestBuilderTest.php index e4fe063..28be568 100644 --- a/tests/unit/RequestBuilder/CampaignRequestBuilderTest.php +++ b/tests/unit/RequestBuilder/CampaignRequestBuilderTest.php @@ -23,9 +23,21 @@ public function shouldBuildRequestWithCommands(): void { $builder = new CampaignRequestBuilder(); - $recommendationCommand1 = UserRecommendation::create('userId1', 1, 'scenario1', 1.0, 600); - $recommendationCommand2 = UserRecommendation::create('userId2', 2, 'scenario2', 0.5, 700); - $recommendationCommand3 = UserRecommendation::create('userId3', 3, 'scenario3', 0.0, 800); + $recommendationCommand1 = UserRecommendation::create('userId1', 'scenario1') + ->setCount(1) + ->setRotationRate(1.0) + ->setRotationTime(600); + + $recommendationCommand2 = UserRecommendation::create('userId2', 'scenario2') + ->setCount(2) + ->setRotationRate(0.5) + ->setRotationTime(700); + + $recommendationCommand3 = UserRecommendation::create('userId3', 'scenario3') + ->setCount(3) + ->setRotationRate(0.0) + ->setRotationTime(800); + $builder->addRecommendation($recommendationCommand1); $builder->addRecommendations([$recommendationCommand2, $recommendationCommand3]); @@ -72,7 +84,12 @@ public function shouldThrowExceptionWhenBatchSizeIsTooBig(): void $builder = new CampaignRequestBuilder(); for ($i = 0; $i < 501; $i++) { - $builder->addRecommendation(UserRecommendation::create('userId1', 1, 'scenario1', 1.0, 600)); + $builder->addRecommendation( + UserRecommendation::create('userId1', 'scenario1') + ->setCount(1) + ->setRotationRate(1.0) + ->setRotationTime(600) + ); $builder->addSorting(Sorting::create('userId1', ['itemId1', 'itemId2'])); } @@ -105,7 +122,12 @@ public function shouldSendRequestViaRequestManager(): void $builder = new CampaignRequestBuilder(); $builder->setRequestManager($requestManagerMock); - $builder->addRecommendation(UserRecommendation::create('userId1', 1, 'scenario1', 1.0, 3600)); + $builder->addRecommendation( + UserRecommendation::create('userId1', 'scenario1') + ->setCount(1) + ->setRotationRate(1.0) + ->setRotationTime(600) + ); $builder->addSorting(Sorting::create('userId1', ['itemId1', 'itemId2'])); $builder->send(); diff --git a/tests/unit/RequestBuilder/RecommendationRequestBuilderTest.php b/tests/unit/RequestBuilder/RecommendationRequestBuilderTest.php index df3db33..b1094cb 100644 --- a/tests/unit/RequestBuilder/RecommendationRequestBuilderTest.php +++ b/tests/unit/RequestBuilder/RecommendationRequestBuilderTest.php @@ -23,7 +23,10 @@ class RecommendationRequestBuilderTest extends TestCase /** @test */ public function shouldBuildRequestWithCommands(): void { - $recommendationsCommand = UserRecommendation::create('userId1', 5, 'test-scenario', 0.5, 3600); + $recommendationsCommand = UserRecommendation::create('userId1', 'test-scenario') + ->setCount(5) + ->setRotationRate(0.5) + ->setRotationTime(3600); $builder = new RecommendationRequestBuilder($recommendationsCommand); $interactionCommand = Interaction::detailView('sourceId1', 'itemId1'); @@ -53,7 +56,10 @@ public function shouldBuildRequestWithCommands(): void /** @test */ public function shouldThrowExceptionWhenSendingCommandsWithoutRequestManager(): void { - $recommendationsCommand = UserRecommendation::create('userId1', 5, 'test-scenario', 0.5, 3600); + $recommendationsCommand = UserRecommendation::create('userId1', 'test-scenario') + ->setCount(5) + ->setRotationRate(0.5) + ->setRotationTime(3600); $builder = new RecommendationRequestBuilder($recommendationsCommand); $this->expectException(LogicException::class); @@ -79,7 +85,10 @@ public function shouldSendRequestViaRequestManager(): void public function shouldThrowExceptionWhenInteractionIsForUnrelatedUser(): void { $builder = new RecommendationRequestBuilder( - $recommendationsCommand = UserRecommendation::create('userId1', 5, 'scenario', 0.5, 3600) + $recommendationsCommand = UserRecommendation::create('userId1', 'scenario') + ->setCount(5) + ->setRotationRate(0.5) + ->setRotationTime(3600) ); $builder->setInteraction(Interaction::purchase('different-user', 'itemId1')); @@ -96,7 +105,10 @@ public function shouldThrowExceptionWhenInteractionIsForUnrelatedUser(): void public function shouldThrowExceptionWhenMergeIsForUnrelatedUser(): void { $builder = new RecommendationRequestBuilder( - $recommendationsCommand = UserRecommendation::create('userId1', 5, 'scenario', 0.5, 3600) + $recommendationsCommand = UserRecommendation::create('userId1', 'scenario') + ->setCount(5) + ->setRotationRate(0.5) + ->setRotationTime(3600) ); $builder->setUserMerge(UserMerge::mergeInto('different-user', 'userId1')); @@ -123,7 +135,10 @@ public function shouldPassOnCorrectSequenceOfUsersWhenMerging( ): void { $interactionCommand = Interaction::purchase($interactionUser, 'test-item-id'); $userMergeCommand = UserMerge::mergeFromSourceToTargetUser($sourceUserToBeDeleted, $targetUserId); - $recommendationsCommand = UserRecommendation::create($recommendationUser, 5, 'scenario', 0.5, 3600); + $recommendationsCommand = UserRecommendation::create($recommendationUser, 'scenario') + ->setCount(5) + ->setRotationRate(0.5) + ->setRotationTime(3600); $builder = new RecommendationRequestBuilder($recommendationsCommand); $builder->setUserMerge($userMergeCommand); @@ -140,7 +155,10 @@ public function shouldFailOnIncorrectSequenceOfUsersWhenMerging(): void { $interactionCommand = Interaction::purchase('test-user-a', 'test-item-id'); $userMergeCommand = UserMerge::mergeFromSourceToTargetUser('test-user-b', 'test-user-a'); - $recommendationsCommand = UserRecommendation::create('test-user-b', 5, 'scenario', 0.5, 3600); + $recommendationsCommand = UserRecommendation::create('test-user-b', 'scenario') + ->setCount(5) + ->setRotationRate(0.5) + ->setRotationTime(3600); $this->expectException(LogicException::class); $this->expectExceptionMessage( diff --git a/tests/unit/RequestBuilder/RequestBuilderFactoryTest.php b/tests/unit/RequestBuilder/RequestBuilderFactoryTest.php index d14d5d2..041235e 100644 --- a/tests/unit/RequestBuilder/RequestBuilderFactoryTest.php +++ b/tests/unit/RequestBuilder/RequestBuilderFactoryTest.php @@ -71,7 +71,10 @@ public function provideBuilderMethods(): array $voidInit = function ($builder): void {}; - $userRecommendation = UserRecommendation::create('user-id', 1, 'test-scenario', 0.5, 3600); + $userRecommendation = UserRecommendation::create('user-id', 'test-scenario') + ->setCount(5) + ->setRotationRate(0.5) + ->setRotationTime(3600); return [ 'getItemProperties' => ['getItemProperties', ItemPropertiesGetRequestBuilder::class, $voidInit], From 47887e06d1509592c6bbc813d8a8a41791e06828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Tesa=CC=81rek?= Date: Sun, 29 Mar 2020 21:27:32 +0200 Subject: [PATCH 3/3] Add support for custom interaction type and their attributes (#107) --- README.md | 22 +- UPGRADE-3.0.md | 88 +++++++- phpstan.neon | 2 - .../Command/Constants/InteractionType.php | 19 -- src/Model/Command/Interaction.php | 151 +++++++------- .../EventsRequestBuilderTest.php | 9 +- .../RequestBuilder/InteractionRequestTest.php | 71 +++++++ .../RecommendationRequestBuilderTest.php | 7 +- .../RequestBuilder/SortingRequestTest.php | 2 +- tests/unit/Model/Command/InteractionTest.php | 192 +++++++++++++++--- .../EventsRequestBuilderTest.php | 8 +- .../RecommendationRequestBuilderTest.php | 8 +- .../SortingRequestBuilderTest.php | 8 +- 13 files changed, 424 insertions(+), 163 deletions(-) delete mode 100644 src/Model/Command/Constants/InteractionType.php create mode 100644 tests/integration/RequestBuilder/InteractionRequestTest.php diff --git a/README.md b/README.md index 8ca70c5..92384ab 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # PHP API Client for Matej recommendation engine [![Latest Stable Version](https://img.shields.io/packagist/v/lmc/matej-client.svg?style=flat-square)](https://packagist.org/packages/lmc/matej-client) @@ -8,7 +9,7 @@ This library requires PHP 7.1+. However, we provide also PHP 5.6-compatible version [`matej-client-php5`](https://github.com/lmc-eu/matej-client-php5). -Please note the PHP 5.6 version is just transpiled copy of this library - examples, pull requests, issues, changelog etc. are placed in this repository. +Please note that the PHP 5.6 version is just transpiled copy of this library - examples, pull requests, issues, changelog etc. are placed in this repository. ## Installation @@ -41,8 +42,7 @@ $ composer require lmc/matej-client php-http/curl-client guzzlehttp/psr7 # use l ## Usage -To start using Matej you will need your account id (database name) and secret API key - both of them must be obtained -from LMC R&D team. +To start using Matej you will need your account id (database name) and secret API key - both of them must be obtained from LMC R&D team. First create an instance of `Matej` object: ```php @@ -61,7 +61,7 @@ Once finished with building the request, use `send()` method to execute it and r ```php $response = $matej->request() ->events() - ->addInteraction(\Lmc\Matej\Model\Command\Interaction::purchase('user-id', 'item-id')) + ->addInteraction(\Lmc\Matej\Model\Command\Interaction::withItem('purchases', 'user-id', 'item-id')) ->addUserMerge(...) ... ->send(); @@ -155,7 +155,7 @@ $matej = new Matej('accountId', 'apikey'); $response = $matej->request() ->events() // Add interaction between user and item - ->addInteraction(Interaction::purchase('user-id', 'item-id')) + ->addInteraction(Interaction::withItem('purchases', 'user-id', 'item-id')) ->addInteractions([/* array of Interaction objects */]) // Update item data ->addItemProperty(ItemProperty::create('item-id', ['valid_from' => time(), 'title' => 'Title'])) @@ -183,7 +183,7 @@ $matej = new Matej('accountId', 'apikey'); $response = $matej->request() ->recommendation(UserRecommendation::create('user-id', 'test-scenario')) - ->setInteraction(Interaction::purchase('user-id', 'item-id')) // optional + ->setInteraction(Interaction::withItem('purchases', 'user-id', 'item-id')) // optional ->setUserMerge(UserMerge::mergeInto('user-id', 'source-id')) // optional ->send(); @@ -232,7 +232,7 @@ $recommendations = $response->getRecommendation()->getData(); // } ``` -You can further modify which items will be reccomended by providing boosting rules. Priority of items matching the +You can further modify which items will be recommended by providing boosting rules. Priority of items matching the MQL `$criteria` will be multiplied by the value of `multiplier`: ```php @@ -277,7 +277,7 @@ $recommendedItems = $response->getRecommendation()->getData(); ``` If you don't specify any response properties, Matej will return an array of `stdClass` instances, which contain only `item_id` property. -If you do request at least one response property, you don't need to metion `item_id`, as Matej will always return it regardless of the +If you do request at least one response property, you don't need to mention `item_id`, as Matej will always return it regardless of the properties requested. If you request an unknown property, Matej will return a `BAD REQUEST` with HTTP status code `400`. @@ -296,7 +296,7 @@ $matej = new Matej('accountId', 'apikey'); $response = $matej->request() ->sorting(Sorting::create('user-id', ['item-id-1', 'item-id-2', 'item-id-3'])) - ->setInteraction(Interaction::purchase('user-id', 'item-id')) // optional + ->setInteraction(Interaction::withItem('purchases', 'user-id', 'item-id')) // optional ->setUserMerge(UserMerge::mergeInto('user-id', 'source-id')) // optional ->send(); @@ -342,7 +342,7 @@ $response = $matej->request() ### A/B Testing support `Recommendation` and `Sorting` commands support optional A/B testing of various models. This has to be set up in Matej first, -but once available, you can specify which model you want to use when requesting recommendations or sortings. +but once available, you can specify which model you want to use when requesting recommendations or sorting. This is available for `recommendation`, `sorting` and `campaign` requests: @@ -373,7 +373,7 @@ $response = $matej->request() If you don't provide any model name, the request will be sent without it, and Matej will use default model for your instance. -Typically, you'd select a random sample of users, to which you'd present recommendations and sortings from second model. This way, implementation +Typically, you'd select a random sample of users, to which you'd present recommendations and sorting from second model. This way, implementation in your code should look similar to this: ```php diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index 6e5da14..60b3deb 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -1,13 +1,19 @@ + # Upgrading from 2.x to 3.0 -API client release 2.0 contains few backward incompatible changes. +API client release 3.0 contains few backward incompatible changes. This guide will help you upgrade your codebase. -## `UserRecommendation::create()` now accepts only `$user_id` and `$scenario` -`UserReccomentation::create()` accepts only two argumens: `$user_id` and `$scenario`. -Both are arguments are required. Reccomendation command can be further parametrized -using fluent API. +## `UserRecommendation::create()` now accepts only `$userId` and `$scenario` +`UserReccomentation::create()` accepts only two arguments: `$user_id` and `$scenario`. + +Both arguments are required. + +All other arguments can be now set using new setters and are optional: +- `setCount(int $count)` +- `setRotationRate(float $rotationRate)` +- `setRotationTime(int $rotationTime)` #### Before ```php @@ -29,4 +35,76 @@ $recommendation = UserRecommendation::create('user-id', 'scenario'); $recommendation->setCount(5); $recommendation->setRotationRate(1.0); $recommendation->setRotationTime(3600); +``` + +## `Interaction` now accepts interaction type as parameter +Matej now allows configuration of custom interaction types and interaction attributes. + +At the same time, it allows specifying interaction item using item alias instead +of item ID. For that reason we removed static constructor methods for creating specific interaction types - `Interaction::detailView`, `Interaction::purchase`, `Interaction::bookmark` and `Interaction::rating`. + +We replaced them with constructors for creating Interaction based on `$itemId` or `$itemIdAlias`: + +```php +Interaction::withItem( + string $interactionType, + string $userId, + string $itemId, + int $timestamp = null +``` + +```php +Interaction::withAliasedItem( + string $interactionType, + string $userId, + array $itemIdAlias, + int $timestamp = null): +``` + +The first argument is always a string representing interaction type. Consult the table bellow to find out the correct value to fill in: + +| Before: constructor method | After: argument $interactionType | +|------------------------------|----------------------------------| +| `Interaction::detailView` | `"detailviews"` | +| `Interaction::purchase` | `"purchases"` | +| `Interaction::bookmark` | `"bookmarks"` | +| `Interaction::rating` | `"ratings"` | + +> To request new interaction types, please contact Matej support. + +#### Before +```php +$interaction = Interaction::bookmark('user-id', 'item_id', time()); +``` + +#### After +```php +$interaction = Interaction::withItem('bookmarks', 'user-id', 'item_id', time()); +``` +> Argument `$timestamp` remains optional. + +## `Interaction` command supports custom attributes +Interactions now support custom attributes. These can be added using fluent API +methods `setAttribute()` and `setAttributes()`. + +Argument `value` was removed from constructor methods and has to be set using new attribute methods. Its real name might have change as well. For example, for interaction type `bookmarks`, it was renamed to `stars`. + +**Attribute `context` is no longer supported and was removed.** + +#### Before +```php +$interaction = Interaction::rating('user-id', 'item_id', 0.5); +``` + +#### After +```php +$interaction = Interaction::create('ratings', 'user-id', 'item_id') + ->setAttribute('stars', 0.5) +``` + +which is equivalent to + +```php +$interaction = Interaction::create('ratings', 'user-id', 'item_id') +$interaction->setAttribute('stars', 0.5) ``` \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index 75dc23e..2f8ae12 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,8 +4,6 @@ parameters: path: tests/ - message: '#Parameter .+ of class Lmc\\Matej\\Matej constructor expects string#' path: tests/integration/IntegrationTestCase.php - - message: '#Parameter .+ of function forward_static_call_array#' - path: tests/unit/Model/Command/InteractionTest.php - message: '#Parameter \#4 \$body of method Http\\Message\\RequestFactory::createRequest\(\) expects#' path: src/Http/RequestManager.php - '#Unsafe usage of new static\(\)#' diff --git a/src/Model/Command/Constants/InteractionType.php b/src/Model/Command/Constants/InteractionType.php deleted file mode 100644 index 995be47..0000000 --- a/src/Model/Command/Constants/InteractionType.php +++ /dev/null @@ -1,19 +0,0 @@ -interactionType = $interactionType; + $this->attributes = new ArrayObject(); + $this->setInteractionType($interactionType); $this->setUserId($userId); + $this->setItemIdAlias($itemIdAlias); $this->setItemId($itemId); - $this->setValue($value); - $this->setContext($context); $this->setTimestamp($timestamp ?? time()); } /** - * Detail view interaction occurs when a user views an information page with detailed description of given item - * (if there is such a feature available in your system). - * - * @return static + * Construct Interaction between user and item identified by ID. */ - public static function detailView( + public static function withItem( + string $interactionType, string $userId, string $itemId, - float $value = 1.0, - string $context = 'default', int $timestamp = null ): self { - return new static(InteractionType::DETAILVIEWS(), $userId, $itemId, $value, $context, $timestamp); - } + $interaction = new static( + $interactionType, $userId, self::DEFAULT_ITEM_ID_ALIAS, $itemId, $timestamp + ); - /** - * Purchase interaction generally refer to buying or downloading a specific item by a user, suggesting that the user - * believes the item to be of high value for her at the time of purchase. For example in the domain of job boards, - * the purchase interaction stands for a reply of the user on specific Job Description. - * - * @return static - */ - public static function purchase( - string $userId, - string $itemId, - float $value = 1.0, - string $context = 'default', - int $timestamp = null - ): self { - return new static(InteractionType::PURCHASES(), $userId, $itemId, $value, $context, $timestamp); + return $interaction; } /** - * If your applications supports bookmarks, eg. flagging items as favorite, you may submit the interactions as well. - * Depending on the nature of your application, bookmarking an item by a user may mean that the user has found the - * item interesting based on: - * - viewing its details, and has added the item to her future "wishlist", - * - viewing its contents, and would like to view it once more in the future. - * In both cases, bookmarking indicates positive relationship of the user to the item, allowing Matej to refine - * recommendations. - * - * @return static + * Construct Interaction between user and item identified by aliased ID. */ - public static function bookmark( + public static function withAliasedItem( + string $interactionType, string $userId, + string $itemIdAlias, string $itemId, - float $value = 1.0, - string $context = 'default', int $timestamp = null ): self { - return new static(InteractionType::BOOKMARKS(), $userId, $itemId, $value, $context, $timestamp); - } + $interaction = new static( + $interactionType, $userId, $itemIdAlias, $itemId, $timestamp + ); - /** - * Ratings are the most valuable type of interaction user may provide to the Matej recommender – they allow users - * to submit explicit evaluations of items. These may be expressed as a number of stars (1-5), 👍/👎 voting etc. - * For the recommendation API, the ratings must be scaled to real-valued interval [0, 1]. - * - * @return static - */ - public static function rating( - string $userId, - string $itemId, - float $value = 1.0, - string $context = 'default', - int $timestamp = null - ): self { - return new static(InteractionType::RATINGS(), $userId, $itemId, $value, $context, $timestamp); + return $interaction; } public function getUserId(): string @@ -121,16 +84,53 @@ public function getCommandType(): string return 'interaction'; } + /** + * Set all Interaction attributes. All previously set attributes are removed. + */ + public function setAttributes(array $attributes): self + { + $this->attributes = new ArrayObject(); + foreach ($attributes as $name => $value) { + $this->setAttribute($name, $value); + } + + return $this; + } + + /** + * Set Interaction attribute and its value. If attribute with the same name + * already exists, it's replaced. + * + * @param mixed $value + */ + public function setAttribute(string $name, $value): self + { + Assertion::typeIdentifier($name); + + $this->attributes[$name] = $value; + + return $this; + } + public function getCommandParameters(): array { - return [ - 'interaction_type' => $this->interactionType->jsonSerialize(), + $common = [ + 'interaction_type' => $this->interactionType, 'user_id' => $this->userId, - 'item_id' => $this->itemId, 'timestamp' => $this->timestamp, - 'value' => $this->value, - 'context' => $this->context, + 'attributes' => $this->attributes, + $this->itemIdAlias => $this->itemId, ]; + $common[$this->itemIdAlias] = $this->itemId; + + return $common; + } + + protected function setInteractionType(string $interactionType): void + { + Assertion::typeIdentifier($interactionType); + + $this->interactionType = $interactionType; } protected function setUserId(string $userId): void @@ -147,18 +147,11 @@ protected function setItemId(string $itemId): void $this->itemId = $itemId; } - protected function setValue(float $value): void - { - Assertion::between($value, 0, 1); - - $this->value = $value; - } - - protected function setContext(string $context): void + protected function setItemIdAlias(string $itemIdAlias): void { - Assertion::typeIdentifier($context); + Assertion::typeIdentifier($itemIdAlias); - $this->context = $context; + $this->itemIdAlias = $itemIdAlias; } protected function setTimestamp(int $timestamp): void diff --git a/tests/integration/RequestBuilder/EventsRequestBuilderTest.php b/tests/integration/RequestBuilder/EventsRequestBuilderTest.php index 1270c4a..88a5f4e 100644 --- a/tests/integration/RequestBuilder/EventsRequestBuilderTest.php +++ b/tests/integration/RequestBuilder/EventsRequestBuilderTest.php @@ -51,11 +51,10 @@ public function shouldExecuteInteractionAndUserMergeAndItemPropertyCommands(): v $response = static::createMatejInstance() ->request() ->events() - ->addInteraction(Interaction::bookmark('user-a', 'item-a')) + ->addInteraction(Interaction::withItem('search', 'user-a', 'item-a')) ->addInteractions([ - Interaction::detailView('user-b', 'item-a'), - Interaction::rating('user-c', 'item-a'), - Interaction::purchase('user-d', 'item-a'), + Interaction::withItem('detailviews', 'user-b', 'item-a'), + Interaction::withItem('purchases', 'user-d', 'item-a'), ]) ->addUserMerge(UserMerge::mergeInto('user-a', 'user-b')) ->addUserMerges([ @@ -69,7 +68,7 @@ public function shouldExecuteInteractionAndUserMergeAndItemPropertyCommands(): v ]) ->send(); - $this->assertResponseCommandStatuses($response, ...$this->generateOkStatuses(10)); + $this->assertResponseCommandStatuses($response, ...$this->generateOkStatuses(9)); } private static function addPropertiesToPropertySetupRequest(ItemPropertiesSetupRequestBuilder $builder): void diff --git a/tests/integration/RequestBuilder/InteractionRequestTest.php b/tests/integration/RequestBuilder/InteractionRequestTest.php new file mode 100644 index 0000000..66e5e96 --- /dev/null +++ b/tests/integration/RequestBuilder/InteractionRequestTest.php @@ -0,0 +1,71 @@ +request() + ->events() + ->addInteraction(Interaction::withItem('invalid-type', 'user-a', 'item-a')) + ->send(); + + $this->assertResponseCommandStatuses($response, 'INVALID'); + } + + /** @test */ + public function shouldSendInteractionWithCustomAttribute(): void + { + $response = static::createMatejInstance() + ->request() + ->events() + ->addInteraction(Interaction::withItem('purchases', 'user-a', 'item-a') + ->setAttribute('quantity', 2) + ) + ->send(); + + $this->assertResponseCommandStatuses($response, 'OK'); + } + + /** @test */ + public function shouldSendInteractionWithCustomAttributes(): void + { + $response = static::createMatejInstance() + ->request() + ->events() + ->addInteraction( + Interaction::withAliasedItem('search', 'user-a', 'search_id', 'search-id') + ->setAttributes([ + 'query' => 'query value', + 'keywords' => 'key, words', + 'serp' => ['serp1', 'serp2'], + 'categories' => ['category1', 'category2'], + 'location' => [['lat' => 41.2395, 'long' => 3.40592]], + 'is_logged_in' => true, + ]) + ) + ->send(); + $this->assertResponseCommandStatuses($response, 'OK'); + } + + /** @test */ + public function shouldSendInteractionWithItemAlias(): void + { + $response = static::createMatejInstance() + ->request() + ->events() + ->addInteraction(Interaction::withAliasedItem('search', 'user-a', 'search_id', 'search-id')) + ->send(); + + $this->assertResponseCommandStatuses($response, 'OK'); + } +} diff --git a/tests/integration/RequestBuilder/RecommendationRequestBuilderTest.php b/tests/integration/RequestBuilder/RecommendationRequestBuilderTest.php index feec868..271528e 100644 --- a/tests/integration/RequestBuilder/RecommendationRequestBuilderTest.php +++ b/tests/integration/RequestBuilder/RecommendationRequestBuilderTest.php @@ -22,7 +22,7 @@ public function shouldExecuteRecommendationRequestOnly(): void $response = static::createMatejInstance() ->request() ->recommendation($this->createRecommendationCommand('user-a') - ->addBoost(Boost::create('test', 1.2)) + ->addBoost(Boost::create('age > 1', 1.2)) )->send(); $this->assertInstanceOf(RecommendationsResponse::class, $response); @@ -37,9 +37,10 @@ public function shouldExecuteRecommendationRequestWithUserMergeAndInteraction(): ->request() ->recommendation($this->createRecommendationCommand('user-b')) ->setUserMerge(UserMerge::mergeInto('user-b', 'user-a')) - ->setInteraction(Interaction::bookmark('user-a', 'item-a')) + ->setInteraction( + Interaction::withItem('detailviews', 'user-a', 'item-a') + ) ->send(); - $this->assertInstanceOf(RecommendationsResponse::class, $response); $this->assertResponseCommandStatuses($response, 'OK', 'OK', 'OK'); $this->assertShorthandResponse($response, 'OK', 'OK', 'OK'); diff --git a/tests/integration/RequestBuilder/SortingRequestTest.php b/tests/integration/RequestBuilder/SortingRequestTest.php index 1ac8694..deb3672 100644 --- a/tests/integration/RequestBuilder/SortingRequestTest.php +++ b/tests/integration/RequestBuilder/SortingRequestTest.php @@ -35,7 +35,7 @@ public function shouldExecuteSortingRequestWithUserMergeAndInteraction(): void ->request() ->sorting(Sorting::create('user-b', ['item-a', 'item-b', 'itemC-c'])) ->setUserMerge(UserMerge::mergeInto('user-b', 'user-a')) - ->setInteraction(Interaction::bookmark('user-a', 'item-a')) + ->setInteraction(Interaction::withItem('detailviews', 'user-a', 'item-a')) ->send(); $this->assertInstanceOf(SortingResponse::class, $response); diff --git a/tests/unit/Model/Command/InteractionTest.php b/tests/unit/Model/Command/InteractionTest.php index 2b6868f..d34988e 100644 --- a/tests/unit/Model/Command/InteractionTest.php +++ b/tests/unit/Model/Command/InteractionTest.php @@ -2,6 +2,8 @@ namespace Lmc\Matej\Model\Command; +use ArrayObject; +use Lmc\Matej\Exception\DomainException; use phpmock\phpunit\PHPMock; use PHPUnit\Framework\TestCase; @@ -20,54 +22,192 @@ public function initTimeMock(): void /** * @test - * @dataProvider provideConstructorName + */ + public function shouldRaiseExceptionWithInvalidAttributeName(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage( + 'Value "invalid^*!@" does not match type identifier format requirement (must contain only of alphanumeric chars, dash or underscore)'); + $command = Interaction::withItem('bookmarks', 'user-id', 'item-id'); + $command->setAttribute('invalid^*!@', 'value'); + } + + /** + * @test + */ + public function shouldRaiseExceptionWithInvalidAttributeNameInBatch(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage( + 'Value "invalid^*!@" does not match type identifier format requirement (must contain only of alphanumeric chars, dash or underscore)'); + $command = Interaction::withItem('bookmarks', 'user-id', 'item-id'); + $command->setAttributes([ + 'valid' => 'value1', + 'invalid^*!@' => 'value2', + ]); + } + + /** + * @test + */ + public function shouldRaiseExceptionWithInvalidInteractionType(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage( + 'Value "invalid^*!@" does not match type identifier format requirement (must contain only of alphanumeric chars, dash or underscore)'); + $command = Interaction::withItem('invalid^*!@', 'user-id', 'item-id'); + } + + /** + * @test + */ + public function shouldRaiseExceptionWithInvalidUserId(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage( + 'Value "invalid^*!@" does not match type identifier format requirement (must contain only of alphanumeric chars, dash or underscore)'); + $command = Interaction::withItem('bookmarks', 'invalid^*!@', 'item-id'); + } + + /** + * @test + */ + public function shouldRaiseExceptionWithInvalidItemId(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage( + 'Value "invalid^*!@" does not match type identifier format requirement (must contain only of alphanumeric chars, dash or underscore)'); + $command = Interaction::withItem('bookmarks', 'user-id', 'invalid^*!@'); + } + + /** + * @test + */ + public function shouldRaiseExceptionWithInvalidItemIdAliasName(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage( + 'Value "invalid^*!@" does not match type identifier format requirement (must contain only of alphanumeric chars, dash or underscore)'); + $command = Interaction::withAliasedItem('bookmarks', 'user-id', 'invalid^*!@', 'item-id'); + } + + /** + * @test + */ + public function shouldRaiseExceptionWithNegativeTimestamp(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Provided "-123" is not greater than "0".'); + $command = Interaction::withItem('bookmarks', 'user-id', 'item-id', -123); + } + + /** + * @test + */ + public function shouldCompileInteractionAttributes(): void + { + $command = Interaction::withItem('bookmarks', 'user-id', 'item-id'); + $command->setAttribute('attribute-1', 'value-1'); + $command->setAttributes([ + 'attribute-2' => 'value-2', + 'attribute-3' => 'value-3', + ]); + $command->setAttribute('attribute-2', 'value-new-2'); + + $this->assertEquals( + $command->jsonSerialize()['parameters']['attributes'], + new ArrayObject([ + 'attribute-2' => 'value-new-2', + 'attribute-3' => 'value-3', + ]) + ); + } + + /** + * @test + * @dataProvider provideItemIdConstructorParams * @runInSeparateProcess so that time() can be mocked safely */ - public function shouldBeInstantiableViaNamedConstructors( - string $constructorName, - string $expectedInteractionType, - array $extraConstructorParams - ): void { - $constructorParams = array_merge(['exampleUserId', 'exampleItemId'], $extraConstructorParams); + public function shouldBeInstantiableWithItemId(array $constructorParams): void + { + /** @var Interaction $command */ + $command = forward_static_call_array( + [Interaction::class, 'withItem'], $constructorParams + ); + + $this->assertInstanceOf(Interaction::class, $command); + $this->assertEquals( + [ + 'type' => 'interaction', + 'parameters' => [ + 'interaction_type' => $constructorParams[0], + 'user_id' => $constructorParams[1], + 'item_id' => $constructorParams[2], + 'timestamp' => $constructorParams[3] ?? static::TIMESTAMP, + 'attributes' => new ArrayObject(), + ], + ], + $command->jsonSerialize() + ); + $this->assertSame($constructorParams[1], $command->getUserId()); + } + /** + * @test + * @dataProvider provideItemIdAliasConstructorParams + * @runInSeparateProcess so that time() can be mocked safely + */ + public function shouldBeInstantiableWithItemIdAlias(array $constructorParams): void + { /** @var Interaction $command */ $command = forward_static_call_array( - [Interaction::class, $constructorName], - $constructorParams + [Interaction::class, 'withAliasedItem'], $constructorParams ); $this->assertInstanceOf(Interaction::class, $command); - $this->assertSame( + $this->assertEquals( [ 'type' => 'interaction', 'parameters' => [ - 'interaction_type' => $expectedInteractionType, - 'user_id' => 'exampleUserId', - 'item_id' => 'exampleItemId', - 'timestamp' => $extraConstructorParams[2] ?? static::TIMESTAMP, - 'value' => $extraConstructorParams[0] ?? 1.0, - 'context' => $extraConstructorParams[1] ?? 'default', + 'interaction_type' => $constructorParams[0], + 'user_id' => $constructorParams[1], + 'timestamp' => $constructorParams[4] ?? static::TIMESTAMP, + 'attributes' => new ArrayObject(), + $constructorParams[2] => $constructorParams[3], ], ], $command->jsonSerialize() ); - $this->assertSame('exampleUserId', $command->getUserId()); + $this->assertSame($constructorParams[1], $command->getUserId()); + } + + /** + * @return array[] + */ + public function provideItemIdConstructorParams(): array + { + return [ + 'with item_id and required params' => [ + ['bookmarks', 'user123', 'item123'], + ], + 'with item_id and optional params' => [ + ['bookmarks', 'user123', 'item123', 123], + ], + ]; } /** * @return array[] */ - public function provideConstructorName(): array + public function provideItemIdAliasConstructorParams(): array { return [ - 'detailView with only required params' => ['detailView', 'detailviews', []], - 'detailView with optional params' => ['detailView', 'detailviews', [0.5, 'myContextFoo', 1337333666]], - 'purchase with only required params' => ['purchase', 'purchases', []], - 'purchase with optional params' => ['purchase', 'purchases', [0.0, 'myContextBar', 1337333666]], - 'bookmark with only required params' => ['bookmark', 'bookmarks', []], - 'bookmark with optional params' => ['bookmark', 'bookmarks', [0.1337, 'myContextBaz', 1337333666]], - 'rating with only required params' => ['rating', 'ratings', []], - 'rating with optional params' => ['rating', 'ratings', [0.9, 'myContextBan', 1337333666]], + 'with single item_id_alias and required params' => [ + ['detailviews', 'user123', 'key', 'value'], + ], + 'with single item_id_alias and optional params' => [ + ['bookmarks', 'user123', 'key', 'value', 123], + ], ]; } } diff --git a/tests/unit/RequestBuilder/EventsRequestBuilderTest.php b/tests/unit/RequestBuilder/EventsRequestBuilderTest.php index c3b6dbd..3d321bd 100644 --- a/tests/unit/RequestBuilder/EventsRequestBuilderTest.php +++ b/tests/unit/RequestBuilder/EventsRequestBuilderTest.php @@ -24,9 +24,9 @@ public function shouldBuildRequestWithCommands(): void { $builder = new EventsRequestBuilder(); - $interactionCommand1 = Interaction::detailView('userId1', 'itemId1'); - $interactionCommand2 = Interaction::bookmark('userId1', 'itemId1'); - $interactionCommand3 = Interaction::purchase('userId1', 'itemId1'); + $interactionCommand1 = Interaction::withItem('detailviews', 'userId1', 'itemId1'); + $interactionCommand2 = Interaction::withItem('bookmarks', 'userId1', 'itemId1'); + $interactionCommand3 = Interaction::withItem('purchases', 'userId1', 'itemId1'); $builder->addInteraction($interactionCommand1); $builder->addInteractions([$interactionCommand2, $interactionCommand3]); @@ -81,7 +81,7 @@ public function shouldThrowExceptionWhenBatchSizeIsTooBig(): void $builder = new EventsRequestBuilder(); for ($i = 0; $i < 334; $i++) { - $builder->addInteraction(Interaction::detailView('userId1', 'itemId1')); + $builder->addInteraction(Interaction::withItem('detailview', 'userId1', 'itemId1')); $builder->addItemProperty(ItemProperty::create('itemId1', ['key1' => 'value1'])); $builder->addUserMerge(UserMerge::mergeFromSourceToTargetUser('sourceId1', 'targetId1')); } diff --git a/tests/unit/RequestBuilder/RecommendationRequestBuilderTest.php b/tests/unit/RequestBuilder/RecommendationRequestBuilderTest.php index b1094cb..da9f39d 100644 --- a/tests/unit/RequestBuilder/RecommendationRequestBuilderTest.php +++ b/tests/unit/RequestBuilder/RecommendationRequestBuilderTest.php @@ -29,7 +29,7 @@ public function shouldBuildRequestWithCommands(): void ->setRotationTime(3600); $builder = new RecommendationRequestBuilder($recommendationsCommand); - $interactionCommand = Interaction::detailView('sourceId1', 'itemId1'); + $interactionCommand = Interaction::withItem('detailviews', 'sourceId1', 'itemId1'); $builder->setInteraction($interactionCommand); $userMergeCommand = UserMerge::mergeFromSourceToTargetUser('sourceId1', 'userId1'); @@ -91,7 +91,7 @@ public function shouldThrowExceptionWhenInteractionIsForUnrelatedUser(): void ->setRotationTime(3600) ); - $builder->setInteraction(Interaction::purchase('different-user', 'itemId1')); + $builder->setInteraction(Interaction::withItem('purchases', 'different-user', 'itemId1')); $this->expectException(LogicException::class); $this->expectExceptionMessage( @@ -133,7 +133,7 @@ public function shouldPassOnCorrectSequenceOfUsersWhenMerging( string $targetUserId, string $recommendationUser ): void { - $interactionCommand = Interaction::purchase($interactionUser, 'test-item-id'); + $interactionCommand = Interaction::withItem('purchases', $interactionUser, 'test-item-id'); $userMergeCommand = UserMerge::mergeFromSourceToTargetUser($sourceUserToBeDeleted, $targetUserId); $recommendationsCommand = UserRecommendation::create($recommendationUser, 'scenario') ->setCount(5) @@ -153,7 +153,7 @@ public function shouldPassOnCorrectSequenceOfUsersWhenMerging( */ public function shouldFailOnIncorrectSequenceOfUsersWhenMerging(): void { - $interactionCommand = Interaction::purchase('test-user-a', 'test-item-id'); + $interactionCommand = Interaction::withItem('purchases', 'test-user-a', 'test-item-id'); $userMergeCommand = UserMerge::mergeFromSourceToTargetUser('test-user-b', 'test-user-a'); $recommendationsCommand = UserRecommendation::create('test-user-b', 'scenario') ->setCount(5) diff --git a/tests/unit/RequestBuilder/SortingRequestBuilderTest.php b/tests/unit/RequestBuilder/SortingRequestBuilderTest.php index 74b49ae..b714d39 100644 --- a/tests/unit/RequestBuilder/SortingRequestBuilderTest.php +++ b/tests/unit/RequestBuilder/SortingRequestBuilderTest.php @@ -26,7 +26,7 @@ public function shouldBuildRequestWithCommands(): void $sortingCommand = Sorting::create('userId1', ['itemId1', 'itemId2']); $builder = new SortingRequestBuilder($sortingCommand); - $interactionCommand = Interaction::detailView('sourceId1', 'itemId1'); + $interactionCommand = Interaction::withItem('detailviews', 'sourceId1', 'itemId1'); $builder->setInteraction($interactionCommand); $userMergeCommand = UserMerge::mergeFromSourceToTargetUser('sourceId1', 'userId1'); @@ -79,7 +79,7 @@ public function shouldThrowExceptionWhenUserOfInteractionDiffersFromSorting(): v { $builder = new SortingRequestBuilder(Sorting::create('userId1', ['itemId1', 'itemId2'])); - $builder->setInteraction(Interaction::purchase('different-user', 'itemId1')); + $builder->setInteraction(Interaction::withItem('purchases', 'different-user', 'itemId1')); $this->expectException(LogicException::class); $this->expectExceptionMessage( @@ -109,7 +109,7 @@ public function shouldThrowExceptionWhenUserOfUserMergeDiffersFromSorting(): voi */ public function shouldPassOnCorrectSequenceOfUsersWhenMerging(): void { - $interactionCommand = Interaction::purchase('test-user-a', 'test-item-id'); + $interactionCommand = Interaction::withItem('purchase', 'test-user-a', 'test-item-id'); $userMergeCommand = UserMerge::mergeFromSourceToTargetUser('test-user-a', 'test-user-b'); $sortingCommand = Sorting::create('test-user-b', ['itemId1', 'itemId2']); @@ -126,7 +126,7 @@ public function shouldPassOnCorrectSequenceOfUsersWhenMerging(): void */ public function shouldFailOnIncorrectSequenceOfUsersWhenMerging(): void { - $interactionCommand = Interaction::purchase('test-user-a', 'test-item-id'); + $interactionCommand = Interaction::withItem('purchase', 'test-user-a', 'test-item-id'); $userMergeCommand = UserMerge::mergeFromSourceToTargetUser('test-user-b', 'test-user-a'); $sortingCommand = Sorting::create('test-user-a', ['itemId1', 'itemId2']);