Skip to content

Commit 95e5fb4

Browse files
authored
Add elasticsearch repository (#47)
1 parent 42253a3 commit 95e5fb4

File tree

4 files changed

+376
-0
lines changed

4 files changed

+376
-0
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Liquetsoft\Fias\Elastic\ElasticSearchRepository;
6+
7+
use Elasticsearch\Client;
8+
use Liquetsoft\Fias\Elastic\ClientProvider\ClientProvider;
9+
use Liquetsoft\Fias\Elastic\Exception\ElasticSearchRepositoryException;
10+
use Liquetsoft\Fias\Elastic\QueryBuilder\QueryBuilder;
11+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
12+
use Throwable;
13+
14+
/**
15+
* Объект, который отправляет запросы на поиск в elasticsearch
16+
* и обрабатывает результаты.
17+
*/
18+
class BaseElasticSearchRepository implements ElasticSearchRepository
19+
{
20+
private ClientProvider $clientProvider;
21+
22+
private DenormalizerInterface $denormalizer;
23+
24+
public function __construct(
25+
ClientProvider $clientProvider,
26+
DenormalizerInterface $denormalizer
27+
) {
28+
$this->clientProvider = $clientProvider;
29+
$this->denormalizer = $denormalizer;
30+
}
31+
32+
/**
33+
* {@inheritDoc}
34+
*/
35+
public function one(QueryBuilder $queryBuilder, string $entityClass): ?object
36+
{
37+
$queryBuilder->size(1);
38+
$res = $this->all($queryBuilder, $entityClass);
39+
40+
return $res[0] ?? null;
41+
}
42+
43+
/**
44+
* {@inheritDoc}
45+
*/
46+
public function all(QueryBuilder $queryBuilder, string $entityClass): array
47+
{
48+
try {
49+
$result = $this->runSearchRequest($queryBuilder, $entityClass);
50+
} catch (Throwable $e) {
51+
throw new ElasticSearchRepositoryException($e->getMessage(), 0, $e);
52+
}
53+
54+
return $result;
55+
}
56+
57+
/**
58+
* Проводит запрос на поиск данных в elasticsearch.
59+
*
60+
* @param QueryBuilder $queryBuilder
61+
* @param string $entityClass
62+
*
63+
* @return object[]
64+
*
65+
* @throws ElasticSearchRepositoryException
66+
*/
67+
private function runSearchRequest(QueryBuilder $queryBuilder, string $entityClass): array
68+
{
69+
$elasticResult = $this->getClient()->search($queryBuilder->getQuery());
70+
$hits = $elasticResult['hits']['hits'] ?? [];
71+
72+
$result = [];
73+
foreach ($hits as $hit) {
74+
$result[] = $this->denormalizeHit($hit, $entityClass);
75+
}
76+
77+
return $result;
78+
}
79+
80+
/**
81+
* Денормализует ответ от elasticsearch в указанный тип объекта.
82+
*
83+
* @param array $hit
84+
* @param string $entityClass
85+
*
86+
* @return object
87+
*/
88+
private function denormalizeHit(array $hit, string $entityClass): object
89+
{
90+
if (empty($hit['_source']) || !\is_array($hit['_source'])) {
91+
$message = "Can't denormalize item from elasticsearch empty source.";
92+
throw new ElasticSearchRepositoryException($message);
93+
}
94+
95+
$object = $this->denormalizer->denormalize($hit['_source'], $entityClass);
96+
97+
if (!\is_object($object)) {
98+
$message = 'Denormalizer returned non-object instance.';
99+
throw new ElasticSearchRepositoryException($message);
100+
}
101+
102+
return $object;
103+
}
104+
105+
/**
106+
* Возвращает объект клиента для запросов к elasticsearch.
107+
*
108+
* @return Client
109+
*/
110+
private function getClient(): Client
111+
{
112+
return $this->clientProvider->provide();
113+
}
114+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Liquetsoft\Fias\Elastic\ElasticSearchRepository;
6+
7+
use Liquetsoft\Fias\Elastic\Exception\ElasticSearchRepositoryException;
8+
use Liquetsoft\Fias\Elastic\QueryBuilder\QueryBuilder;
9+
10+
/**
11+
* Интерфейс для объекта, который отправляет запросы на поиск в elasticsearch
12+
* и обрабатывает результаты.
13+
*/
14+
interface ElasticSearchRepository
15+
{
16+
/**
17+
* Ищет только одну запись по условию.
18+
*
19+
* @param QueryBuilder $queryBuilder
20+
* @param string $entityClass
21+
*
22+
* @return object|null
23+
*
24+
* @throws ElasticSearchRepositoryException
25+
*/
26+
public function one(QueryBuilder $queryBuilder, string $entityClass): ?object;
27+
28+
/**
29+
* Отправляет запрос на поиск в elasticsearch
30+
* и преобразует ответ в соответствующий массив объектов.
31+
*
32+
* @param QueryBuilder $queryBuilder
33+
* @param string $entityClass
34+
*
35+
* @return object[]
36+
*
37+
* @throws ElasticSearchRepositoryException
38+
*/
39+
public function all(QueryBuilder $queryBuilder, string $entityClass): array;
40+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Liquetsoft\Fias\Elastic\Exception;
6+
7+
/**
8+
* Исключение, которое выбрасывается при ошибке выборки из elastic.
9+
*/
10+
class ElasticSearchRepositoryException extends Exception
11+
{
12+
}
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Liquetsoft\Fias\Elastic\Tests\ElasticSearchRepository;
6+
7+
use Elasticsearch\Client;
8+
use Liquetsoft\Fias\Elastic\ClientProvider\ClientProvider;
9+
use Liquetsoft\Fias\Elastic\ElasticSearchRepository\BaseElasticSearchRepository;
10+
use Liquetsoft\Fias\Elastic\Exception\ElasticSearchRepositoryException;
11+
use Liquetsoft\Fias\Elastic\QueryBuilder\QueryBuilder;
12+
use Liquetsoft\Fias\Elastic\Tests\BaseCase;
13+
use stdClass;
14+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
15+
use Throwable;
16+
17+
/**
18+
* Тест для репозитория elasticsearch.
19+
*
20+
* @internal
21+
*/
22+
class BaseElasticSearchRepositoryTest extends BaseCase
23+
{
24+
/**
25+
* Проверяет, что репозтиторий правильно вернет один объект по условию.
26+
*
27+
* @throws Throwable
28+
*/
29+
public function testOne(): void
30+
{
31+
$class = 'test';
32+
$queryData = [
33+
'test' => 'value',
34+
];
35+
$hits = [
36+
'hits' => [
37+
'hits' => [
38+
[
39+
'_source' => ['test' => 'value'],
40+
],
41+
],
42+
],
43+
];
44+
45+
$query = $this->getMockBuilder(QueryBuilder::class)->getMock();
46+
$query->method('getQuery')->willReturn($queryData);
47+
48+
$client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock();
49+
$client->expects($this->once())
50+
->method('search')
51+
->with(
52+
$this->identicalTo($queryData)
53+
)
54+
->willReturn($hits)
55+
;
56+
57+
$clientProvider = $this->getMockBuilder(ClientProvider::class)->getMock();
58+
$clientProvider->method('provide')->willReturn($client);
59+
60+
$object = new stdClass();
61+
62+
$denormalizer = $this->getMockBuilder(DenormalizerInterface::class)->getMock();
63+
$denormalizer->expects($this->once())
64+
->method('denormalize')
65+
->with(
66+
$this->identicalTo($hits['hits']['hits'][0]['_source']),
67+
$this->identicalTo($class)
68+
)
69+
->willReturn($object)
70+
;
71+
72+
$repo = new BaseElasticSearchRepository($clientProvider, $denormalizer);
73+
$testObject = $repo->one($query, $class);
74+
75+
$this->assertSame($object, $testObject);
76+
}
77+
78+
/**
79+
* Проверяет, что репозтиторий правильно вернет список объектов по условию.
80+
*
81+
* @throws Throwable
82+
*/
83+
public function testAll(): void
84+
{
85+
$class = 'test';
86+
$queryData = [
87+
'test' => 'value',
88+
];
89+
$hits = [
90+
'hits' => [
91+
'hits' => [
92+
[
93+
'_source' => ['test' => 'value'],
94+
],
95+
[
96+
'_source' => ['test1' => 'value 1'],
97+
],
98+
],
99+
],
100+
];
101+
102+
$query = $this->getMockBuilder(QueryBuilder::class)->getMock();
103+
$query->method('getQuery')->willReturn($queryData);
104+
105+
$client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock();
106+
$client->expects($this->once())
107+
->method('search')
108+
->with(
109+
$this->identicalTo($queryData)
110+
)
111+
->willReturn($hits)
112+
;
113+
114+
$clientProvider = $this->getMockBuilder(ClientProvider::class)->getMock();
115+
$clientProvider->method('provide')->willReturn($client);
116+
117+
$object = new stdClass();
118+
$object1 = new stdClass();
119+
120+
$denormalizer = $this->getMockBuilder(DenormalizerInterface::class)->getMock();
121+
$denormalizer->method('denormalize')
122+
->willReturnMap(
123+
[
124+
[$hits['hits']['hits'][0]['_source'], $class, null, [], $object],
125+
[$hits['hits']['hits'][1]['_source'], $class, null, [], $object1],
126+
]
127+
)
128+
;
129+
130+
$repo = new BaseElasticSearchRepository($clientProvider, $denormalizer);
131+
$testObjects = $repo->all($query, $class);
132+
133+
$this->assertSame([$object, $object1], $testObjects);
134+
}
135+
136+
/**
137+
* Проверяет, что репозтиторий выбросит исключение при неполном ответе от elasticsearch.
138+
*
139+
* @throws Throwable
140+
*/
141+
public function testEmptySourceException(): void
142+
{
143+
$class = 'test';
144+
$queryData = [
145+
'test' => 'value',
146+
];
147+
$hits = [
148+
'hits' => [
149+
'hits' => [
150+
[],
151+
],
152+
],
153+
];
154+
155+
$query = $this->getMockBuilder(QueryBuilder::class)->getMock();
156+
$query->method('getQuery')->willReturn($queryData);
157+
158+
$client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock();
159+
$client->method('search')->willReturn($hits);
160+
161+
$clientProvider = $this->getMockBuilder(ClientProvider::class)->getMock();
162+
$clientProvider->method('provide')->willReturn($client);
163+
164+
$denormalizer = $this->getMockBuilder(DenormalizerInterface::class)->getMock();
165+
166+
$repo = new BaseElasticSearchRepository($clientProvider, $denormalizer);
167+
168+
$this->expectException(ElasticSearchRepositoryException::class);
169+
$repo->one($query, $class);
170+
}
171+
172+
/**
173+
* Проверяет, что репозтиторий выбросит исключение при неправильном ответе от denprmalizer.
174+
*
175+
* @throws Throwable
176+
*/
177+
public function testBrokenDenormalizeException(): void
178+
{
179+
$class = 'test';
180+
$queryData = [
181+
'test' => 'value',
182+
];
183+
$hits = [
184+
'hits' => [
185+
'hits' => [
186+
[
187+
'_source' => ['test' => 'value'],
188+
],
189+
],
190+
],
191+
];
192+
193+
$query = $this->getMockBuilder(QueryBuilder::class)->getMock();
194+
$query->method('getQuery')->willReturn($queryData);
195+
196+
$client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock();
197+
$client->method('search')->willReturn($hits);
198+
199+
$clientProvider = $this->getMockBuilder(ClientProvider::class)->getMock();
200+
$clientProvider->method('provide')->willReturn($client);
201+
202+
$denormalizer = $this->getMockBuilder(DenormalizerInterface::class)->getMock();
203+
$denormalizer->method('denormalize')->willReturn('test');
204+
205+
$repo = new BaseElasticSearchRepository($clientProvider, $denormalizer);
206+
207+
$this->expectException(ElasticSearchRepositoryException::class);
208+
$repo->one($query, $class);
209+
}
210+
}

0 commit comments

Comments
 (0)