Skip to content

Commit

Permalink
Add elasticsearch repository (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
marvin255 authored May 17, 2021
1 parent 42253a3 commit 95e5fb4
Show file tree
Hide file tree
Showing 4 changed files with 376 additions and 0 deletions.
114 changes: 114 additions & 0 deletions src/ElasticSearchRepository/BaseElasticSearchRepository.php
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();
}
}
40 changes: 40 additions & 0 deletions src/ElasticSearchRepository/ElasticSearchRepository.php
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;
}
12 changes: 12 additions & 0 deletions src/Exception/ElasticSearchRepositoryException.php
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 tests/ElasticSearchRepository/BaseElasticSearchRepositoryTest.php
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);
}
}

0 comments on commit 95e5fb4

Please sign in to comment.