Skip to content

Commit 946b543

Browse files
committed
EV-267: Added index service
1 parent 39f74f8 commit 946b543

File tree

8 files changed

+225
-27
lines changed

8 files changed

+225
-27
lines changed

baseline.xml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<files psalm-version="5.16.0@2897ba636551a8cb61601cc26f6ccfbba6c36591">
3+
<file src="src/Api/State/EventRepresentationProvider.php">
4+
<MissingTemplateParam>
5+
<code>ProviderInterface</code>
6+
</MissingTemplateParam>
7+
</file>
8+
<file src="src/Command/FixturesLoadCommand.php">
9+
<UndefinedDocblockClass>
10+
<code>\HttpException</code>
11+
</UndefinedDocblockClass>
12+
</file>
13+
<file src="src/Fixtures/FixtureLoader.php">
14+
<InvalidArgument>
15+
<code><![CDATA[[
16+
'index' => $indexName,
17+
'body' => [
18+
'settings' => [
19+
'number_of_shards' => 5,
20+
'number_of_replicas' => 0,
21+
],
22+
],
23+
]]]></code>
24+
</InvalidArgument>
25+
<PossiblyUndefinedMethod>
26+
<code>getStatusCode</code>
27+
<code>getStatusCode</code>
28+
</PossiblyUndefinedMethod>
29+
<UndefinedClass>
30+
<code>\HttpException</code>
31+
</UndefinedClass>
32+
<UndefinedDocblockClass>
33+
<code>\HttpException</code>
34+
<code>\HttpException</code>
35+
</UndefinedDocblockClass>
36+
</file>
37+
<file src="src/Service/EsIndex.php">
38+
<UndefinedClass>
39+
<code>\IndexInterface</code>
40+
</UndefinedClass>
41+
</file>
42+
</files>

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"php": ">=8.2",
1010
"ext-ctype": "*",
1111
"ext-iconv": "*",
12+
"ext-http": "*",
1213
"api-platform/core": "^3.2",
1314
"doctrine/doctrine-bundle": "^2.11",
1415
"doctrine/doctrine-migrations-bundle": "^3.3",

src/Command/FixturesLoadCommand.php

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

55
use App\Fixtures\FixtureLoader;
66
use App\Model\IndexNames;
7+
use Elastic\Elasticsearch\Exception\ClientResponseException;
8+
use Elastic\Elasticsearch\Exception\MissingParameterException;
9+
use Elastic\Elasticsearch\Exception\ServerResponseException;
710
use Symfony\Component\Console\Attribute\AsCommand;
811
use Symfony\Component\Console\Command\Command;
912
use Symfony\Component\Console\Completion\CompletionInput;
@@ -12,6 +15,10 @@
1215
use Symfony\Component\Console\Input\InputOption;
1316
use Symfony\Component\Console\Output\OutputInterface;
1417
use Symfony\Component\Console\Style\SymfonyStyle;
18+
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
19+
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
20+
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
21+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
1522

1623
#[AsCommand(
1724
name: 'app:fixtures:load',
@@ -31,35 +38,46 @@ protected function configure(): void
3138
$this->addArgument(
3239
'index',
3340
InputArgument::REQUIRED,
34-
sprintf('Index to dump (one of %s)', implode(', ', IndexNames::values())),
41+
sprintf('Index to populate with fixture data (one of %s)', implode(', ', IndexNames::values())),
3542
null,
3643
function (CompletionInput $input): array {
3744
return array_filter(IndexNames::values(), fn ($item) => str_starts_with($item, $input->getCompletionValue()));
3845
}
3946
)
40-
->addOption('url', null, InputOption::VALUE_OPTIONAL, 'Remote url to read data from', 'https://raw.githubusercontent.com/itk-dev/event-database-imports/develop/src/DataFixtures/indexes/[index].json');
47+
->addOption('url', null, InputOption::VALUE_OPTIONAL, 'Remote url to read fixture data from', 'https://raw.githubusercontent.com/itk-dev/event-database-imports/develop/src/DataFixtures/indexes/[index].json');
4148
}
4249

50+
/**
51+
* @throws RedirectionExceptionInterface
52+
* @throws ClientExceptionInterface
53+
* @throws \JsonException
54+
* @throws TransportExceptionInterface
55+
* @throws ClientResponseException
56+
* @throws ServerExceptionInterface
57+
* @throws \HttpException
58+
* @throws ServerResponseException
59+
* @throws MissingParameterException
60+
*/
4361
protected function execute(InputInterface $input, OutputInterface $output): int
4462
{
4563
$io = new SymfonyStyle($input, $output);
46-
$index = $input->getArgument('index');
64+
$indexName = $input->getArgument('index');
4765
$url = (string) $input->getOption('url');
48-
$url = strtr($url, ['[index]' => $index]);
66+
$url = strtr($url, ['[index]' => $indexName]);
4967

5068
if ('dev' !== $this->appEnv) {
5169
$io->error('This command should only be executed in development environment.');
5270

5371
return Command::FAILURE;
5472
}
5573

56-
if (!in_array($index, IndexNames::values())) {
57-
$io->error(sprintf('Index %s does not exist', $index));
74+
if (!in_array($indexName, IndexNames::values())) {
75+
$io->error(sprintf('Index %s does not exist', $indexName));
5876

5977
return Command::FAILURE;
6078
}
6179

62-
$this->loader->process($index, $url);
80+
$this->loader->process($indexName, $url);
6381

6482
return Command::SUCCESS;
6583
}

src/Exception/AppException.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace App\Exception;
4+
5+
abstract class AppException extends \Exception
6+
{
7+
}

src/Exception/IndexException.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace App\Exception;
4+
5+
class IndexException extends AppException
6+
{
7+
}

src/Fixtures/FixtureLoader.php

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,101 @@
22

33
namespace App\Fixtures;
44

5+
use App\Service\IndexInterface;
56
use Elastic\Elasticsearch\Client;
67
use Elastic\Elasticsearch\Exception\ClientResponseException;
78
use Elastic\Elasticsearch\Exception\MissingParameterException;
89
use Elastic\Elasticsearch\Exception\ServerResponseException;
910
use Symfony\Component\HttpFoundation\Response;
11+
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
12+
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
13+
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
14+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
1015
use Symfony\Contracts\HttpClient\HttpClientInterface;
1116

1217
class FixtureLoader
1318
{
1419
public function __construct(
1520
private readonly HttpClientInterface $httpClient,
21+
private readonly IndexInterface $index,
1622
private readonly Client $client
1723
) {
1824
}
1925

26+
/**
27+
* @throws RedirectionExceptionInterface
28+
* @throws ClientExceptionInterface
29+
* @throws \JsonException
30+
* @throws TransportExceptionInterface
31+
* @throws ClientResponseException
32+
* @throws ServerExceptionInterface
33+
* @throws \HttpException
34+
* @throws ServerResponseException
35+
* @throws MissingParameterException
36+
* @throws \Exception
37+
*/
2038
public function process(string $indexName, string $url): void
2139
{
2240
$items = $this->download($url);
2341

24-
$configuration = [
25-
'index' => $indexName,
26-
'body' => [
27-
'settings' => [
28-
'number_of_shards' => 5,
29-
'number_of_replicas' => 0,
30-
],
31-
],
32-
];
42+
$this->createIndex($indexName);
43+
$this->indexItems($indexName, $items);
44+
}
45+
46+
/**
47+
* Download data as JSON from a given URL.
48+
*
49+
* @param string $url
50+
* The URL from which to download the data
51+
*
52+
* @return array
53+
* The downloaded data as an associative array
54+
*
55+
* @throws \HttpException
56+
* If unable to download the fixture data
57+
* @throws \JsonException
58+
* If there is an error, decoding the downloaded data
59+
* @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface
60+
* If a client exception occurs during the HTTP request
61+
* @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface
62+
* If a redirection exception occurs during the HTTP request
63+
* @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface
64+
* If a server exception occurs during the HTTP request
65+
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
66+
* If a transport exception occurs during the HTTP request
67+
*/
68+
private function download(string $url): array
69+
{
70+
$response = $this->httpClient->request('GET', $url);
3371

34-
try {
35-
$response = $this->client->indices()->create($configuration);
36-
} catch (ClientResponseException|MissingParameterException|ServerResponseException $e) {
37-
// Ignore index exists error.
72+
if (Response::HTTP_OK !== $response->getStatusCode()) {
73+
throw new \HttpException('Unable to download fixture data');
3874
}
3975

76+
return json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
77+
}
78+
79+
/**
80+
* Index items in Elasticsearch.
81+
*
82+
* @param string $indexName
83+
* The name of the index in Elasticsearch where the items should be indexed
84+
* @param array $items
85+
* The items to be indexed in Elasticsearch. Each item should be an associative array.
86+
*
87+
* @throws \Exception
88+
* If unable to add an item to the index
89+
*/
90+
private function indexItems(string $indexName, array $items): void
91+
{
4092
foreach ($items as $item) {
4193
$params = [
4294
'index' => $indexName,
4395
'id' => $item['entityId'],
4496
'body' => $item,
4597
];
4698
try {
99+
// No other places in this part of the frontend should index data, hence it's not in the index service.
47100
$response = $this->client->index($params);
48101

49102
if (!in_array($response->getStatusCode(), [Response::HTTP_OK, Response::HTTP_CREATED, Response::HTTP_NO_CONTENT])) {
@@ -55,14 +108,33 @@ public function process(string $indexName, string $url): void
55108
}
56109
}
57110

58-
private function download($url): array
111+
/**
112+
* Creates an index with the given name if it does not already exist.
113+
*
114+
* @param string $indexName
115+
* The name of the index
116+
*
117+
* @throws clientResponseException
118+
* If an error occurs during the Elasticsearch client request
119+
* @throws missingParameterException
120+
* If the required parameter is missing
121+
* @throws serverResponseException
122+
* If the server returns an error during the Elasticsearch request
123+
*/
124+
private function createIndex(string $indexName): void
59125
{
60-
$response = $this->httpClient->request('GET', $url);
61-
62-
if (Response::HTTP_OK !== $response->getStatusCode()) {
63-
throw new \HttpException('Unable to download fixture data');
126+
if (!$this->index->indexExists($indexName)) {
127+
// This creation of the index is not in den index service as this is the only place it should be used. In
128+
// production and in many cases, you should connect to the index managed by the backend (imports).
129+
$this->client->indices()->create([
130+
'index' => $indexName,
131+
'body' => [
132+
'settings' => [
133+
'number_of_shards' => 5,
134+
'number_of_replicas' => 0,
135+
],
136+
],
137+
]);
64138
}
65-
66-
return json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
67139
}
68140
}

src/Service/ElasticSearchIndex.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace App\Service;
4+
5+
use App\Exception\IndexException;
6+
use Elastic\Elasticsearch\Client;
7+
use Elastic\Elasticsearch\Exception\ClientResponseException;
8+
use Elastic\Elasticsearch\Exception\ServerResponseException;
9+
use Elastic\Elasticsearch\Response\Elasticsearch;
10+
use Symfony\Component\HttpFoundation\Response;
11+
12+
class ElasticSearchIndex implements \IndexInterface
13+
{
14+
public function __construct(
15+
private readonly Client $client
16+
) {
17+
}
18+
19+
public function indexExists($indexName): bool
20+
{
21+
try {
22+
/** @var Elasticsearch $response */
23+
$response = $this->client->indices()->getAlias(['name' => $indexName]);
24+
25+
return Response::HTTP_OK === $response->getStatusCode();
26+
} catch (ClientResponseException|ServerResponseException $e) {
27+
if (Response::HTTP_NOT_FOUND === $e->getCode()) {
28+
return false;
29+
}
30+
31+
throw new IndexException($e->getMessage(), $e->getCode(), $e);
32+
}
33+
}
34+
}

src/Service/IndexInterface.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace App\Service;
4+
5+
interface IndexInterface
6+
{
7+
/**
8+
* Checks if the given index exists.
9+
*
10+
* @param string $indexName
11+
* The name of the index to check.
12+
*
13+
* @return bool
14+
* True if the index exists, false otherwise.
15+
*/
16+
public function indexExists(string $indexName): bool;
17+
}

0 commit comments

Comments
 (0)