-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
376 additions
and
0 deletions.
There are no files selected for viewing
114 changes: 114 additions & 0 deletions
114
src/ElasticSearchRepository/BaseElasticSearchRepository.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Liquetsoft\Fias\Elastic\ElasticSearchRepository; | ||
|
||
use Elasticsearch\Client; | ||
use Liquetsoft\Fias\Elastic\ClientProvider\ClientProvider; | ||
use Liquetsoft\Fias\Elastic\Exception\ElasticSearchRepositoryException; | ||
use Liquetsoft\Fias\Elastic\QueryBuilder\QueryBuilder; | ||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | ||
use Throwable; | ||
|
||
/** | ||
* Объект, который отправляет запросы на поиск в elasticsearch | ||
* и обрабатывает результаты. | ||
*/ | ||
class BaseElasticSearchRepository implements ElasticSearchRepository | ||
{ | ||
private ClientProvider $clientProvider; | ||
|
||
private DenormalizerInterface $denormalizer; | ||
|
||
public function __construct( | ||
ClientProvider $clientProvider, | ||
DenormalizerInterface $denormalizer | ||
) { | ||
$this->clientProvider = $clientProvider; | ||
$this->denormalizer = $denormalizer; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function one(QueryBuilder $queryBuilder, string $entityClass): ?object | ||
{ | ||
$queryBuilder->size(1); | ||
$res = $this->all($queryBuilder, $entityClass); | ||
|
||
return $res[0] ?? null; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function all(QueryBuilder $queryBuilder, string $entityClass): array | ||
{ | ||
try { | ||
$result = $this->runSearchRequest($queryBuilder, $entityClass); | ||
} catch (Throwable $e) { | ||
throw new ElasticSearchRepositoryException($e->getMessage(), 0, $e); | ||
} | ||
|
||
return $result; | ||
} | ||
|
||
/** | ||
* Проводит запрос на поиск данных в elasticsearch. | ||
* | ||
* @param QueryBuilder $queryBuilder | ||
* @param string $entityClass | ||
* | ||
* @return object[] | ||
* | ||
* @throws ElasticSearchRepositoryException | ||
*/ | ||
private function runSearchRequest(QueryBuilder $queryBuilder, string $entityClass): array | ||
{ | ||
$elasticResult = $this->getClient()->search($queryBuilder->getQuery()); | ||
$hits = $elasticResult['hits']['hits'] ?? []; | ||
|
||
$result = []; | ||
foreach ($hits as $hit) { | ||
$result[] = $this->denormalizeHit($hit, $entityClass); | ||
} | ||
|
||
return $result; | ||
} | ||
|
||
/** | ||
* Денормализует ответ от elasticsearch в указанный тип объекта. | ||
* | ||
* @param array $hit | ||
* @param string $entityClass | ||
* | ||
* @return object | ||
*/ | ||
private function denormalizeHit(array $hit, string $entityClass): object | ||
{ | ||
if (empty($hit['_source']) || !\is_array($hit['_source'])) { | ||
$message = "Can't denormalize item from elasticsearch empty source."; | ||
throw new ElasticSearchRepositoryException($message); | ||
} | ||
|
||
$object = $this->denormalizer->denormalize($hit['_source'], $entityClass); | ||
|
||
if (!\is_object($object)) { | ||
$message = 'Denormalizer returned non-object instance.'; | ||
throw new ElasticSearchRepositoryException($message); | ||
} | ||
|
||
return $object; | ||
} | ||
|
||
/** | ||
* Возвращает объект клиента для запросов к elasticsearch. | ||
* | ||
* @return Client | ||
*/ | ||
private function getClient(): Client | ||
{ | ||
return $this->clientProvider->provide(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Liquetsoft\Fias\Elastic\ElasticSearchRepository; | ||
|
||
use Liquetsoft\Fias\Elastic\Exception\ElasticSearchRepositoryException; | ||
use Liquetsoft\Fias\Elastic\QueryBuilder\QueryBuilder; | ||
|
||
/** | ||
* Интерфейс для объекта, который отправляет запросы на поиск в elasticsearch | ||
* и обрабатывает результаты. | ||
*/ | ||
interface ElasticSearchRepository | ||
{ | ||
/** | ||
* Ищет только одну запись по условию. | ||
* | ||
* @param QueryBuilder $queryBuilder | ||
* @param string $entityClass | ||
* | ||
* @return object|null | ||
* | ||
* @throws ElasticSearchRepositoryException | ||
*/ | ||
public function one(QueryBuilder $queryBuilder, string $entityClass): ?object; | ||
|
||
/** | ||
* Отправляет запрос на поиск в elasticsearch | ||
* и преобразует ответ в соответствующий массив объектов. | ||
* | ||
* @param QueryBuilder $queryBuilder | ||
* @param string $entityClass | ||
* | ||
* @return object[] | ||
* | ||
* @throws ElasticSearchRepositoryException | ||
*/ | ||
public function all(QueryBuilder $queryBuilder, string $entityClass): array; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Liquetsoft\Fias\Elastic\Exception; | ||
|
||
/** | ||
* Исключение, которое выбрасывается при ошибке выборки из elastic. | ||
*/ | ||
class ElasticSearchRepositoryException extends Exception | ||
{ | ||
} |
210 changes: 210 additions & 0 deletions
210
tests/ElasticSearchRepository/BaseElasticSearchRepositoryTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Liquetsoft\Fias\Elastic\Tests\ElasticSearchRepository; | ||
|
||
use Elasticsearch\Client; | ||
use Liquetsoft\Fias\Elastic\ClientProvider\ClientProvider; | ||
use Liquetsoft\Fias\Elastic\ElasticSearchRepository\BaseElasticSearchRepository; | ||
use Liquetsoft\Fias\Elastic\Exception\ElasticSearchRepositoryException; | ||
use Liquetsoft\Fias\Elastic\QueryBuilder\QueryBuilder; | ||
use Liquetsoft\Fias\Elastic\Tests\BaseCase; | ||
use stdClass; | ||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | ||
use Throwable; | ||
|
||
/** | ||
* Тест для репозитория elasticsearch. | ||
* | ||
* @internal | ||
*/ | ||
class BaseElasticSearchRepositoryTest extends BaseCase | ||
{ | ||
/** | ||
* Проверяет, что репозтиторий правильно вернет один объект по условию. | ||
* | ||
* @throws Throwable | ||
*/ | ||
public function testOne(): void | ||
{ | ||
$class = 'test'; | ||
$queryData = [ | ||
'test' => 'value', | ||
]; | ||
$hits = [ | ||
'hits' => [ | ||
'hits' => [ | ||
[ | ||
'_source' => ['test' => 'value'], | ||
], | ||
], | ||
], | ||
]; | ||
|
||
$query = $this->getMockBuilder(QueryBuilder::class)->getMock(); | ||
$query->method('getQuery')->willReturn($queryData); | ||
|
||
$client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock(); | ||
$client->expects($this->once()) | ||
->method('search') | ||
->with( | ||
$this->identicalTo($queryData) | ||
) | ||
->willReturn($hits) | ||
; | ||
|
||
$clientProvider = $this->getMockBuilder(ClientProvider::class)->getMock(); | ||
$clientProvider->method('provide')->willReturn($client); | ||
|
||
$object = new stdClass(); | ||
|
||
$denormalizer = $this->getMockBuilder(DenormalizerInterface::class)->getMock(); | ||
$denormalizer->expects($this->once()) | ||
->method('denormalize') | ||
->with( | ||
$this->identicalTo($hits['hits']['hits'][0]['_source']), | ||
$this->identicalTo($class) | ||
) | ||
->willReturn($object) | ||
; | ||
|
||
$repo = new BaseElasticSearchRepository($clientProvider, $denormalizer); | ||
$testObject = $repo->one($query, $class); | ||
|
||
$this->assertSame($object, $testObject); | ||
} | ||
|
||
/** | ||
* Проверяет, что репозтиторий правильно вернет список объектов по условию. | ||
* | ||
* @throws Throwable | ||
*/ | ||
public function testAll(): void | ||
{ | ||
$class = 'test'; | ||
$queryData = [ | ||
'test' => 'value', | ||
]; | ||
$hits = [ | ||
'hits' => [ | ||
'hits' => [ | ||
[ | ||
'_source' => ['test' => 'value'], | ||
], | ||
[ | ||
'_source' => ['test1' => 'value 1'], | ||
], | ||
], | ||
], | ||
]; | ||
|
||
$query = $this->getMockBuilder(QueryBuilder::class)->getMock(); | ||
$query->method('getQuery')->willReturn($queryData); | ||
|
||
$client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock(); | ||
$client->expects($this->once()) | ||
->method('search') | ||
->with( | ||
$this->identicalTo($queryData) | ||
) | ||
->willReturn($hits) | ||
; | ||
|
||
$clientProvider = $this->getMockBuilder(ClientProvider::class)->getMock(); | ||
$clientProvider->method('provide')->willReturn($client); | ||
|
||
$object = new stdClass(); | ||
$object1 = new stdClass(); | ||
|
||
$denormalizer = $this->getMockBuilder(DenormalizerInterface::class)->getMock(); | ||
$denormalizer->method('denormalize') | ||
->willReturnMap( | ||
[ | ||
[$hits['hits']['hits'][0]['_source'], $class, null, [], $object], | ||
[$hits['hits']['hits'][1]['_source'], $class, null, [], $object1], | ||
] | ||
) | ||
; | ||
|
||
$repo = new BaseElasticSearchRepository($clientProvider, $denormalizer); | ||
$testObjects = $repo->all($query, $class); | ||
|
||
$this->assertSame([$object, $object1], $testObjects); | ||
} | ||
|
||
/** | ||
* Проверяет, что репозтиторий выбросит исключение при неполном ответе от elasticsearch. | ||
* | ||
* @throws Throwable | ||
*/ | ||
public function testEmptySourceException(): void | ||
{ | ||
$class = 'test'; | ||
$queryData = [ | ||
'test' => 'value', | ||
]; | ||
$hits = [ | ||
'hits' => [ | ||
'hits' => [ | ||
[], | ||
], | ||
], | ||
]; | ||
|
||
$query = $this->getMockBuilder(QueryBuilder::class)->getMock(); | ||
$query->method('getQuery')->willReturn($queryData); | ||
|
||
$client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock(); | ||
$client->method('search')->willReturn($hits); | ||
|
||
$clientProvider = $this->getMockBuilder(ClientProvider::class)->getMock(); | ||
$clientProvider->method('provide')->willReturn($client); | ||
|
||
$denormalizer = $this->getMockBuilder(DenormalizerInterface::class)->getMock(); | ||
|
||
$repo = new BaseElasticSearchRepository($clientProvider, $denormalizer); | ||
|
||
$this->expectException(ElasticSearchRepositoryException::class); | ||
$repo->one($query, $class); | ||
} | ||
|
||
/** | ||
* Проверяет, что репозтиторий выбросит исключение при неправильном ответе от denprmalizer. | ||
* | ||
* @throws Throwable | ||
*/ | ||
public function testBrokenDenormalizeException(): void | ||
{ | ||
$class = 'test'; | ||
$queryData = [ | ||
'test' => 'value', | ||
]; | ||
$hits = [ | ||
'hits' => [ | ||
'hits' => [ | ||
[ | ||
'_source' => ['test' => 'value'], | ||
], | ||
], | ||
], | ||
]; | ||
|
||
$query = $this->getMockBuilder(QueryBuilder::class)->getMock(); | ||
$query->method('getQuery')->willReturn($queryData); | ||
|
||
$client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock(); | ||
$client->method('search')->willReturn($hits); | ||
|
||
$clientProvider = $this->getMockBuilder(ClientProvider::class)->getMock(); | ||
$clientProvider->method('provide')->willReturn($client); | ||
|
||
$denormalizer = $this->getMockBuilder(DenormalizerInterface::class)->getMock(); | ||
$denormalizer->method('denormalize')->willReturn('test'); | ||
|
||
$repo = new BaseElasticSearchRepository($clientProvider, $denormalizer); | ||
|
||
$this->expectException(ElasticSearchRepositoryException::class); | ||
$repo->one($query, $class); | ||
} | ||
} |