Skip to content

Commit 1f2b127

Browse files
authored
Merge pull request #51 from answear/ANS-18915-recommendations-request-ok
ANS-18915 - recommendations request
2 parents dcf44e0 + a111966 commit 1f2b127

File tree

12 files changed

+239
-11
lines changed

12 files changed

+239
-11
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Answear\LuigisBoxBundle\Factory;
6+
7+
use Answear\LuigisBoxBundle\Service\ConfigProvider;
8+
use Answear\LuigisBoxBundle\Service\LuigisBoxSerializer;
9+
use Answear\LuigisBoxBundle\ValueObject\ObjectsInterface;
10+
use GuzzleHttp\Psr7\Request;
11+
use GuzzleHttp\Psr7\Uri;
12+
13+
class RecommendationsFactory extends AbstractFactory
14+
{
15+
private const HTTP_METHOD = 'POST';
16+
private const ENDPOINT = '/' . ConfigProvider::API_VERSION . '/recommend';
17+
18+
public function __construct(
19+
private ConfigProvider $configProvider,
20+
private LuigisBoxSerializer $serializer,
21+
) {
22+
parent::__construct($configProvider, $serializer);
23+
}
24+
25+
public function prepareRequest(ObjectsInterface $bodyObject): Request
26+
{
27+
$now = \DateTime::createFromFormat('U', (string) time());
28+
29+
return new Request(
30+
$this->getHttpMethod(),
31+
new Uri(
32+
sprintf(
33+
'%s?tracker_id=%s',
34+
$this->configProvider->getHost() . self::ENDPOINT,
35+
$this->configProvider->getPublicKey(),
36+
)
37+
),
38+
$this->configProvider->getAuthorizationHeaders($this->getHttpMethod(), $this->getEndpoint(), $now),
39+
$this->serializer->serialize($bodyObject)
40+
);
41+
}
42+
43+
protected function getHttpMethod(): string
44+
{
45+
return self::HTTP_METHOD;
46+
}
47+
48+
protected function getEndpoint(): string
49+
{
50+
return self::ENDPOINT;
51+
}
52+
}

src/LuigisBoxBundle/Response/ApiResponse.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public function __construct(
2828
array $response,
2929
) {
3030
$this->rawResponse = $response;
31-
$this->okCount = (int) $response[self::OK_COUNT_PARAM];
31+
$this->okCount = (int) ($response[self::OK_COUNT_PARAM] ?? 0);
3232
$this->errorsCount = (int) ($response[self::ERRORS_COUNT_PARAM] ?? 0);
3333

3434
$success = false;

src/LuigisBoxBundle/Service/LuigisBoxSerializer.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Answear\LuigisBoxBundle\Service;
66

7+
use Answear\LuigisBoxBundle\ValueObject\ArrayWrapInterface;
78
use Answear\LuigisBoxBundle\ValueObject\ObjectsInterface;
89
use Symfony\Component\Serializer\Encoder\JsonEncoder;
910
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
@@ -14,15 +15,15 @@ class LuigisBoxSerializer
1415
{
1516
private const SERIALIZE_FORMAT = 'json';
1617

17-
public function serialize(ObjectsInterface $objects): string
18+
public function serialize(ObjectsInterface|ArrayWrapInterface $objects): string
1819
{
1920
$encoders = [new JsonEncoder()];
2021
$normalizers = [new Normalizer\PropertyNormalizer(null, new CamelCaseToSnakeCaseNameConverter())];
2122

2223
$serializer = new Serializer($normalizers, $encoders);
2324

2425
return $serializer->serialize(
25-
$objects,
26+
$objects instanceof ArrayWrapInterface ? $objects->getObjects() : $objects,
2627
self::SERIALIZE_FORMAT,
2728
[Normalizer\AbstractObjectNormalizer::SKIP_NULL_VALUES => true]
2829
);

src/LuigisBoxBundle/Service/Request.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
use Answear\LuigisBoxBundle\Factory\ContentRemovalFactory;
1313
use Answear\LuigisBoxBundle\Factory\ContentUpdateFactory;
1414
use Answear\LuigisBoxBundle\Factory\PartialContentUpdateFactory;
15+
use Answear\LuigisBoxBundle\Factory\RecommendationsFactory;
1516
use Answear\LuigisBoxBundle\Response\ApiResponse;
1617
use Answear\LuigisBoxBundle\ValueObject\ContentAvailability;
1718
use Answear\LuigisBoxBundle\ValueObject\ContentAvailabilityCollection;
1819
use Answear\LuigisBoxBundle\ValueObject\ContentRemovalCollection;
1920
use Answear\LuigisBoxBundle\ValueObject\ContentUpdate;
2021
use Answear\LuigisBoxBundle\ValueObject\ContentUpdateCollection;
2122
use Answear\LuigisBoxBundle\ValueObject\PartialContentUpdate;
23+
use Answear\LuigisBoxBundle\ValueObject\RecommendationsCollection;
2224
use Psr\Http\Message\ResponseInterface;
2325
use Webmozart\Assert\Assert;
2426

@@ -32,6 +34,7 @@ public function __construct(
3234
private ContentUpdateFactory $contentUpdateFactory,
3335
private PartialContentUpdateFactory $partialContentUpdateFactory,
3436
private ContentRemovalFactory $contentRemovalFactory,
37+
private RecommendationsFactory $recommendationsFactory,
3538
) {
3639
}
3740

@@ -117,6 +120,25 @@ public function changeAvailability($object): ApiResponse
117120
);
118121
}
119122

123+
/**
124+
* @throws BadRequestException
125+
* @throws TooManyRequestsException
126+
* @throws TooManyItemsException
127+
* @throws ServiceUnavailableException
128+
* @throws MalformedResponseException
129+
*
130+
* @experimental This feature is in an experimental stage. Breaking changes may occur without prior notice.
131+
*/
132+
public function getRecommendations(RecommendationsCollection $recommendationsCollection): ApiResponse
133+
{
134+
$request = $this->recommendationsFactory->prepareRequest($recommendationsCollection);
135+
136+
return new ApiResponse(
137+
0,
138+
$this->handleResponse($request, $this->client->request($request))
139+
);
140+
}
141+
120142
public static function getContentUpdateLimit(): int
121143
{
122144
return self::CONTENT_UPDATE_OBJECTS_LIMIT;

src/LuigisBoxBundle/Service/RequestInterface.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Answear\LuigisBoxBundle\Response\ApiResponse;
88
use Answear\LuigisBoxBundle\ValueObject\ContentRemovalCollection;
99
use Answear\LuigisBoxBundle\ValueObject\ContentUpdateCollection;
10+
use Answear\LuigisBoxBundle\ValueObject\RecommendationsCollection;
1011

1112
interface RequestInterface
1213
{
@@ -17,4 +18,9 @@ public function partialContentUpdate(ContentUpdateCollection $objects): ApiRespo
1718
public function contentRemoval(ContentRemovalCollection $objects): ApiResponse;
1819

1920
public function changeAvailability($object): ApiResponse;
21+
22+
/**
23+
* @experimental This feature is in an experimental stage. Breaking changes may occur without prior notice.
24+
*/
25+
public function getRecommendations(RecommendationsCollection $recommendationsCollection): ApiResponse;
2026
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Answear\LuigisBoxBundle\ValueObject;
6+
7+
interface ArrayWrapInterface
8+
{
9+
public function getObjects(): array;
10+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Answear\LuigisBoxBundle\ValueObject;
6+
7+
readonly class Recommendation
8+
{
9+
public function __construct(
10+
public string $recommendationType,
11+
public ?string $userId = null,
12+
public ?array $hitFields = null,
13+
public ?array $itemIds = null,
14+
public ?array $blacklistedItemIds = null,
15+
public ?int $size = null,
16+
public ?array $recommendationContext = null,
17+
public ?array $settingsOverride = null,
18+
public ?bool $markFallbackResults = null,
19+
public ?string $recommenderClientIdentifier = null,
20+
) {
21+
}
22+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Answear\LuigisBoxBundle\ValueObject;
6+
7+
use Webmozart\Assert\Assert;
8+
9+
readonly class RecommendationsCollection implements ObjectsInterface, ArrayWrapInterface, \Countable
10+
{
11+
/**
12+
* @param Recommendation[] $objects
13+
*/
14+
public function __construct(private array $objects)
15+
{
16+
Assert::allIsInstanceOf($objects, Recommendation::class);
17+
}
18+
19+
public function count(): int
20+
{
21+
return \count($this->objects);
22+
}
23+
24+
public function getObjects(): array
25+
{
26+
return $this->objects;
27+
}
28+
}

tests/Acceptance/Service/RequestTest.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Answear\LuigisBoxBundle\Factory\ContentRemovalFactory;
88
use Answear\LuigisBoxBundle\Factory\ContentUpdateFactory;
99
use Answear\LuigisBoxBundle\Factory\PartialContentUpdateFactory;
10+
use Answear\LuigisBoxBundle\Factory\RecommendationsFactory;
1011
use Answear\LuigisBoxBundle\Service\Client;
1112
use Answear\LuigisBoxBundle\Service\ConfigProvider;
1213
use Answear\LuigisBoxBundle\Service\LuigisBoxSerializer;
@@ -17,6 +18,7 @@
1718
use Answear\LuigisBoxBundle\ValueObject\ContentAvailability;
1819
use Answear\LuigisBoxBundle\ValueObject\ContentRemovalCollection;
1920
use Answear\LuigisBoxBundle\ValueObject\ContentUpdateCollection;
21+
use Answear\LuigisBoxBundle\ValueObject\RecommendationsCollection;
2022
use GuzzleHttp\Psr7\Response;
2123
use GuzzleHttp\Psr7\Uri;
2224
use PHPUnit\Framework\Attributes\DataProviderExternal;
@@ -109,13 +111,29 @@ public function changeAvailabilityRequestPassed(
109111
self::assertSame([], $response->errors);
110112
}
111113

114+
#[Test]
115+
#[DataProviderExternal(RequestDataProvider::class, 'forRecommendationsRequest')]
116+
public function getRecommendationsRequestPassed(
117+
string $httpMethod,
118+
RecommendationsCollection $collection,
119+
string $expectedContent,
120+
array $apiResponse,
121+
): void {
122+
$response = $this
123+
->getRequestService($httpMethod, $expectedContent, $apiResponse, '/v1/recommend')
124+
->getRecommendations($collection);
125+
126+
self::assertTrue($response->isSuccess());
127+
self::assertSame(0, $response->errorsCount);
128+
self::assertSame([], $response->errors);
129+
}
130+
112131
private function getRequestService(
113132
string $httpMethod,
114133
string $expectedContent,
115134
array $apiResponse,
135+
string $endpoint = '/v1/content',
116136
): RequestInterface {
117-
$endpoint = '/v1/content';
118-
119137
$expectedRequest = new \GuzzleHttp\Psr7\Request(
120138
$httpMethod,
121139
new Uri('host' . $endpoint),
@@ -171,6 +189,7 @@ static function (\GuzzleHttp\Psr7\Request $currentRequest) use ($expectedRequest
171189
new ContentUpdateFactory($this->configProvider, $serializer),
172190
new PartialContentUpdateFactory($this->configProvider, $serializer),
173191
new ContentRemovalFactory($this->configProvider, $serializer),
192+
new RecommendationsFactory($this->configProvider, $serializer)
174193
);
175194
}
176195
}

tests/DataProvider/RequestDataProvider.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use Answear\LuigisBoxBundle\ValueObject\ContentUpdate;
1212
use Answear\LuigisBoxBundle\ValueObject\ContentUpdateCollection;
1313
use Answear\LuigisBoxBundle\ValueObject\PartialContentUpdate;
14+
use Answear\LuigisBoxBundle\ValueObject\Recommendation;
15+
use Answear\LuigisBoxBundle\ValueObject\RecommendationsCollection;
1416

1517
class RequestDataProvider
1618
{
@@ -154,4 +156,63 @@ public static function forChangeAvailability(): iterable
154156
],
155157
];
156158
}
159+
160+
public static function forRecommendationsRequest(): iterable
161+
{
162+
yield [
163+
'POST',
164+
new RecommendationsCollection([new Recommendation('user_conversion_based', '1234')]),
165+
'[{"recommendation_type":"user_conversion_based","user_id":"1234"}]',
166+
[
167+
[
168+
'generated_at' => '2024-12-16T15:18:36.434588',
169+
'hits' => [
170+
[
171+
'attributes' => [
172+
'title' => 'Title',
173+
],
174+
'nested' => [],
175+
'type' => 'product',
176+
'url' => '/p/title-id',
177+
],
178+
],
179+
'model_version' => null,
180+
'recommendation_id' => '111',
181+
'recommendation_type' => 'user_conversion_based',
182+
'recommender' => 'user_conversion_based',
183+
'recommender_client_identifier' => 'user_conversion_based',
184+
'recommender_version' => '111',
185+
'user_id' => '1234',
186+
],
187+
],
188+
];
189+
190+
yield [
191+
'POST',
192+
new RecommendationsCollection([new Recommendation('user_conversion_based', '1234', hitFields: ['url', 'title'])]),
193+
'[{"recommendation_type":"user_conversion_based","user_id":"1234","hit_fields":["url","title"]}]',
194+
[
195+
[
196+
'generated_at' => '2024-12-16T15:18:36.434588',
197+
'hits' => [
198+
[
199+
'attributes' => [
200+
'title' => 'title 2',
201+
],
202+
'nested' => [],
203+
'type' => 'product',
204+
'url' => '/p/title-2-id',
205+
],
206+
],
207+
'model_version' => null,
208+
'recommendation_id' => '111',
209+
'recommendation_type' => 'user_conversion_based',
210+
'recommender' => 'user_conversion_based',
211+
'recommender_client_identifier' => 'user_conversion_based',
212+
'recommender_version' => '111',
213+
'user_id' => '1234',
214+
],
215+
],
216+
];
217+
}
157218
}

0 commit comments

Comments
 (0)