Skip to content

Commit 89c0f30

Browse files
Add reading support for Elasticsearch (#1999)
* Elasticsearch reading support * Fix add the missing dependencies to the SortExtension service * Fix tests and some minor refactoring * Fix duplicate properties * Fix CS * Fix PHPStan errors * Replace the deprecated nested_path with the new nested sort option * Disable write operations by default * Convert snake_case document fields to lowerCamelCase * Add unit tests * Add functional tests * Update deprecations * Fix CS * Fix Roland's comment * Fix er1z's comments * Fix Kévin's comments * Update the changelog
1 parent 6d13f30 commit 89c0f30

File tree

89 files changed

+6801
-350
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+6801
-350
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ jobs:
186186
name: Run Behat tests
187187
command: |-
188188
mkdir -p build/logs/tmp build/cov
189-
for f in $(find features -name '*.feature' -not -path 'features/main/exposed_state.feature' | circleci tests split --split-by=timings); do
189+
for f in $(find features -name '*.feature' -not -path 'features/main/exposed_state.feature' -not -path 'features/elasticsearch/*' | circleci tests split --split-by=timings); do
190190
_f=${f//\//_}
191191
FEATURE="${_f}" phpdbg -qrr vendor/bin/behat --profile=coverage --suite=default --format=progress --out=std --format=junit --out=build/logs/tmp/"${_f}" "$f"
192192
done

.php_cs.dist

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ HEADER;
1313

1414
$finder = PhpCsFixer\Finder::create()
1515
->in(__DIR__)
16-
->exclude('tests/Fixtures/app/cache')
1716
->exclude('tests/Fixtures/app/var')
1817
;
1918

.travis.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ jobs:
99
include:
1010
- php: '7.1'
1111
- php: '7.2'
12+
- php: '7.2'
13+
services:
14+
- elasticsearch
15+
env: APP_ENV=elasticsearch
1216
- php: '7.3'
1317
- php: '7.3'
1418
env: deps=low
@@ -31,6 +35,9 @@ jobs:
3135
fast_finish: true
3236

3337
before_install:
38+
- if [[ $APP_ENV = 'elasticsearch' ]]; then
39+
curl -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.5.0.deb && sudo dpkg -i --force-confnew elasticsearch-6.5.0.deb && sudo service elasticsearch restart;
40+
fi
3441
- phpenv config-rm xdebug.ini || echo "xdebug not available"
3542
- echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
3643
- export PATH="$PATH:$HOME/.composer/vendor/bin"
@@ -48,6 +55,8 @@ script:
4855
- tests/Fixtures/app/console cache:clear
4956
- if [[ $APP_ENV = 'postgres' ]]; then
5057
vendor/bin/behat --suite=postgres --format=progress;
58+
elif [[ $APP_ENV = 'elasticsearch' ]]; then
59+
vendor/bin/behat --suite=elasticsearch --format=progress;
5160
else
5261
vendor/bin/behat --suite=default --format=progress;
5362
fi

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 2.4.0
4+
5+
* Elasticsearch: add reading support (including pagination, sort filter and term filter)
6+
37
## 2.3.5
48

59
* GraphQL: compatibility with `webonyx/graphql-php` 0.13

behat.yml.dist

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,56 @@ default:
22
suites:
33
default:
44
contexts:
5-
- 'FeatureContext': { doctrine: '@doctrine' }
5+
- 'DoctrineContext':
6+
doctrine: '@doctrine'
7+
- 'HttpHeaderContext'
68
- 'GraphqlContext'
79
- 'JsonContext'
810
- 'HydraContext'
911
- 'SwaggerContext'
1012
- 'HttpCacheContext'
11-
- 'JsonApiContext': { doctrine: '@doctrine', jsonApiSchemaFile: 'tests/Fixtures/JsonSchema/jsonapi.json' }
12-
- 'JsonHalContext': { schemaFile: 'tests/Fixtures/JsonHal/jsonhal.json' }
13+
- 'JsonApiContext':
14+
doctrine: '@doctrine'
15+
jsonApiSchemaFile: '%paths.base%/tests/Fixtures/JsonSchema/jsonapi.json'
16+
- 'JsonHalContext':
17+
schemaFile: '%paths.base%/tests/Fixtures/JsonHal/jsonhal.json'
1318
- 'Behat\MinkExtension\Context\MinkContext'
1419
- 'Behatch\Context\RestContext'
1520
filters:
16-
tags: ~@postgres
21+
tags: '~@postgres&&~@elasticsearch'
1722
postgres:
1823
contexts:
19-
- 'FeatureContext': { doctrine: '@doctrine' }
24+
- 'DoctrineContext':
25+
doctrine: '@doctrine'
26+
- 'HttpHeaderContext'
2027
- 'GraphqlContext'
2128
- 'JsonContext'
2229
- 'HydraContext'
2330
- 'SwaggerContext'
2431
- 'HttpCacheContext'
25-
- 'JsonApiContext': { doctrine: '@doctrine', jsonApiSchemaFile: 'tests/Fixtures/JsonSchema/jsonapi.json' }
26-
- 'JsonHalContext': { schemaFile: 'tests/Fixtures/JsonHal/jsonhal.json' }
32+
- 'JsonApiContext':
33+
doctrine: '@doctrine'
34+
jsonApiSchemaFile: '%paths.base%/tests/Fixtures/JsonSchema/jsonapi.json'
35+
- 'JsonHalContext':
36+
schemaFile: '%paths.base%/tests/Fixtures/JsonHal/jsonhal.json'
2737
- 'Behat\MinkExtension\Context\MinkContext'
2838
- 'Behatch\Context\RestContext'
2939
filters:
30-
tags: ~@sqlite
40+
tags: '~@sqlite&&~@elasticsearch'
41+
elasticsearch:
42+
paths:
43+
- '%paths.base%/features/elasticsearch'
44+
contexts:
45+
- 'ElasticsearchContext':
46+
client: '@test.api_platform.elasticsearch.client'
47+
elasticsearchMappingsPath: '%paths.base%/tests/Fixtures/Elasticsearch/Mappings/'
48+
elasticsearchFixturesPath: '%paths.base%/tests/Fixtures/Elasticsearch/Fixtures/'
49+
- 'HttpHeaderContext'
50+
- 'JsonContext'
51+
- 'Behatch\Context\RestContext'
52+
- 'Behat\MinkExtension\Context\MinkContext'
53+
filters:
54+
tags: '@elasticsearch'
3155
extensions:
3256
'Behat\Symfony2Extension':
3357
kernel:
@@ -36,7 +60,7 @@ default:
3660
path: 'tests/Fixtures/app/AppKernel.php'
3761
bootstrap: 'tests/Fixtures/app/bootstrap.php'
3862
'Behat\MinkExtension':
39-
base_url: "http://example.com/"
63+
base_url: 'http://example.com/'
4064
sessions:
4165
default:
4266
symfony2: ~
@@ -46,16 +70,21 @@ coverage:
4670
suites:
4771
default:
4872
contexts:
49-
- 'FeatureContext': { doctrine: '@doctrine' }
73+
- 'DoctrineContext':
74+
doctrine: '@doctrine'
75+
- 'HttpHeaderContext'
5076
- 'GraphqlContext'
5177
- 'JsonContext'
5278
- 'HydraContext'
5379
- 'SwaggerContext'
5480
- 'HttpCacheContext'
55-
- 'JsonApiContext': { doctrine: '@doctrine', jsonApiSchemaFile: 'tests/Fixtures/JsonSchema/jsonapi.json' }
56-
- 'JsonHalContext': { schemaFile: 'tests/Fixtures/JsonHal/jsonhal.json' }
81+
- 'JsonApiContext':
82+
doctrine: '@doctrine'
83+
jsonApiSchemaFile: '%paths.base%/tests/Fixtures/JsonSchema/jsonapi.json'
84+
- 'JsonHalContext':
85+
schemaFile: '%paths.base%/tests/Fixtures/JsonHal/jsonhal.json'
5786
- 'CoverageContext'
5887
- 'Behat\MinkExtension\Context\MinkContext'
5988
- 'Behatch\Context\RestContext'
6089
filters:
61-
tags: ~@postgres
90+
tags: '~@postgres&&~@elasticsearch'

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"doctrine/doctrine-cache-bundle": "^1.3.5",
3737
"doctrine/doctrine-bundle": "^1.8",
3838
"doctrine/orm": "^2.6.3",
39+
"elasticsearch/elasticsearch": "^6.0",
3940
"friendsofsymfony/user-bundle": "^2.1",
4041
"guzzlehttp/guzzle": "^6.0",
4142
"justinrainbow/json-schema": "^5.0",
@@ -76,6 +77,7 @@
7677
"doctrine/common": "<2.7"
7778
},
7879
"suggest": {
80+
"elasticsearch/elasticsearch": "To support Elasticsearch.",
7981
"friendsofsymfony/user-bundle": "To use the FOSUserBundle bridge.",
8082
"guzzlehttp/guzzle": "To use the HTTP cache invalidation system.",
8183
"phpdocumentor/reflection-docblock": "To support extracting metadata from PHPDoc.",

features/bootstrap/FeatureContext.php renamed to features/bootstrap/DoctrineContext.php

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,14 @@
5454
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\User;
5555
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\UuidIdentifierDummy;
5656
use Behat\Behat\Context\Context;
57-
use Behat\Behat\Context\SnippetAcceptingContext;
58-
use Behat\Behat\Hook\Scope\AfterStepScope;
59-
use Behatch\HttpCall\Request;
6057
use Doctrine\Common\Persistence\ManagerRegistry;
6158
use Doctrine\ORM\EntityManagerInterface;
6259
use Doctrine\ORM\Tools\SchemaTool;
6360

6461
/**
6562
* Defines application features from the specific context.
6663
*/
67-
final class FeatureContext implements Context, SnippetAcceptingContext
64+
final class DoctrineContext implements Context
6865
{
6966
/**
7067
* @var EntityManagerInterface
@@ -73,7 +70,6 @@ final class FeatureContext implements Context, SnippetAcceptingContext
7370
private $doctrine;
7471
private $schemaTool;
7572
private $classes;
76-
private $request;
7773

7874
/**
7975
* Initializes context.
@@ -82,35 +78,12 @@ final class FeatureContext implements Context, SnippetAcceptingContext
8278
* You can also pass arbitrary arguments to the
8379
* context constructor through behat.yml.
8480
*/
85-
public function __construct(ManagerRegistry $doctrine, Request $request)
81+
public function __construct(ManagerRegistry $doctrine)
8682
{
8783
$this->doctrine = $doctrine;
8884
$this->manager = $doctrine->getManager();
8985
$this->schemaTool = new SchemaTool($this->manager);
9086
$this->classes = $this->manager->getMetadataFactory()->getAllMetadata();
91-
$this->request = $request;
92-
}
93-
94-
/**
95-
* Sets the default Accept HTTP header to null (workaround to artificially remove it).
96-
*
97-
* @AfterStep
98-
*/
99-
public function removeAcceptHeaderAfterRequest(AfterStepScope $event)
100-
{
101-
if (preg_match('/^I send a "[A-Z]+" request to ".+"/', $event->getStep()->getText())) {
102-
$this->request->setHttpHeader('Accept', null);
103-
}
104-
}
105-
106-
/**
107-
* Sets the default Accept HTTP header to null (workaround to artificially remove it).
108-
*
109-
* @BeforeScenario
110-
*/
111-
public function removeAcceptHeaderBeforeScenario()
112-
{
113-
$this->request->setHttpHeader('Accept', null);
11487
}
11588

11689
/**
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\DocumentMetadata;
15+
use Behat\Behat\Context\Context;
16+
use Elasticsearch\Client;
17+
use Symfony\Component\Finder\Finder;
18+
19+
/**
20+
* @experimental
21+
*
22+
* @author Baptiste Meyer <[email protected]>
23+
*/
24+
final class ElasticsearchContext implements Context
25+
{
26+
private $client;
27+
private $elasticsearchMappingsPath;
28+
private $elasticsearchFixturesPath;
29+
30+
public function __construct(Client $client, string $elasticsearchMappingsPath, string $elasticsearchFixturesPath)
31+
{
32+
$this->client = $client;
33+
$this->elasticsearchMappingsPath = $elasticsearchMappingsPath;
34+
$this->elasticsearchFixturesPath = $elasticsearchFixturesPath;
35+
}
36+
37+
/**
38+
* @BeforeScenario
39+
*/
40+
public function initializeElasticsearch(): void
41+
{
42+
static $initialized = false;
43+
44+
if ($initialized) {
45+
return;
46+
}
47+
48+
$this->deleteIndexes();
49+
$this->createIndexesAndMappings();
50+
$this->loadFixtures();
51+
52+
$initialized = true;
53+
}
54+
55+
/**
56+
* @Given indexes and their mappings are created
57+
*/
58+
public function thereAreIndexes(): void
59+
{
60+
$this->createIndexesAndMappings();
61+
}
62+
63+
/**
64+
* @Given indexes are deleted
65+
*/
66+
public function thereAreNoIndexes(): void
67+
{
68+
$this->deleteIndexes();
69+
}
70+
71+
/**
72+
* @Given fixtures files are loaded
73+
*/
74+
public function thereAreFixtures(): void
75+
{
76+
$this->loadFixtures();
77+
}
78+
79+
private function createIndexesAndMappings(): void
80+
{
81+
$finder = new Finder();
82+
$finder->files()->in($this->elasticsearchMappingsPath);
83+
84+
foreach ($finder as $file) {
85+
$this->client->indices()->create([
86+
'index' => $file->getBasename('.json'),
87+
'body' => json_decode($file->getContents(), true),
88+
]);
89+
}
90+
}
91+
92+
private function deleteIndexes(): void
93+
{
94+
$finder = new Finder();
95+
$finder->files()->in($this->elasticsearchMappingsPath)->name('*.json');
96+
97+
$indexClient = $this->client->indices();
98+
99+
foreach ($finder as $file) {
100+
$index = $file->getBasename('.json');
101+
if (!$indexClient->exists(['index' => $index])) {
102+
continue;
103+
}
104+
105+
$indexClient->delete(['index' => $index]);
106+
}
107+
}
108+
109+
private function loadFixtures(): void
110+
{
111+
$finder = new Finder();
112+
$finder->files()->in($this->elasticsearchFixturesPath)->name('*.json');
113+
114+
$indexClient = $this->client->indices();
115+
116+
foreach ($finder as $file) {
117+
$index = $file->getBasename('.json');
118+
$bulk = [];
119+
120+
foreach (json_decode($file->getContents(), true) as $document) {
121+
if (null === $document['id'] ?? null) {
122+
$bulk[] = ['index' => ['_index' => $index, '_type' => DocumentMetadata::DEFAULT_TYPE]];
123+
} else {
124+
$bulk[] = ['create' => ['_index' => $index, '_type' => DocumentMetadata::DEFAULT_TYPE, '_id' => (string) $document['id']]];
125+
}
126+
127+
$bulk[] = $document;
128+
129+
if (0 === (count($bulk) % 50)) {
130+
$this->client->bulk(['body' => $bulk]);
131+
$bulk = [];
132+
}
133+
}
134+
135+
if ($bulk) {
136+
$this->client->bulk(['body' => $bulk]);
137+
}
138+
139+
$indexClient->refresh(['index' => $index]);
140+
}
141+
}
142+
}

0 commit comments

Comments
 (0)