Skip to content

Commit acade92

Browse files
authored
Added mapTo feature, see #1398 (#1399)
* Added mapTo feature, see #1398 * Fixed php-http/message-factory issue and extended memory to 256M for phpstan
1 parent f70c97a commit acade92

File tree

5 files changed

+187
-4
lines changed

5 files changed

+187
-4
lines changed

composer.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
"symfony/finder": "~4.0",
2727
"nyholm/psr7": "^1.5",
2828
"php-http/mock-client": "^1.5",
29-
"symfony/http-client": "^5.0|^6.0",
30-
"psr/http-factory" : "^1.0"
29+
"symfony/http-client": "^5.0|^6.0|^7.0",
30+
"psr/http-factory" : "^1.0",
31+
"php-http/message-factory" : "^1.0"
3132
},
3233
"autoload": {
3334
"psr-4": {
@@ -51,7 +52,7 @@
5152
"vendor/bin/phpunit --testdox -c phpunit-integration-cloud-tests.xml"
5253
],
5354
"phpstan": [
54-
"phpstan analyse src --level 2 --no-progress"
55+
"phpstan analyse src --level 2 --no-progress --memory-limit 256M"
5556
]
5657
},
5758
"config": {

src/Response/Elasticsearch.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@
1515
namespace Elastic\Elasticsearch\Response;
1616

1717
use ArrayAccess;
18+
use DateTime;
1819
use Elastic\Elasticsearch\Exception\ArrayAccessException;
1920
use Elastic\Elasticsearch\Exception\ClientResponseException;
2021
use Elastic\Elasticsearch\Exception\ServerResponseException;
2122
use Elastic\Elasticsearch\Traits\MessageResponseTrait;
2223
use Elastic\Elasticsearch\Traits\ProductCheckTrait;
24+
use Elastic\Elasticsearch\Utility;
2325
use Elastic\Transport\Exception\UnknownContentTypeException;
2426
use Elastic\Transport\Serializer\CsvSerializer;
2527
use Elastic\Transport\Serializer\JsonSerializer;
2628
use Elastic\Transport\Serializer\NDJsonSerializer;
2729
use Elastic\Transport\Serializer\XmlSerializer;
2830
use Psr\Http\Message\ResponseInterface;
31+
use stdClass;
2932

3033
/**
3134
* Wraps a PSR-7 ResponseInterface offering helpers to deserialize the body response
@@ -224,4 +227,63 @@ public function offsetUnset($offset): void
224227
{
225228
throw new ArrayAccessException('The array is reading only');
226229
}
230+
231+
/**
232+
* Map the response body to an object of a specific class
233+
* by default the class is the PHP standard one (stdClass)
234+
*
235+
* This mapping works only for ES|QL results (with columns and values)
236+
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html
237+
*
238+
* @return object[]
239+
*/
240+
public function mapTo(string $class = stdClass::class): array
241+
{
242+
$response = $this->asArray();
243+
if (!isset($response['columns']) || !isset($response['values'])) {
244+
throw new UnknownContentTypeException(sprintf(
245+
"The response is not a valid ES|QL result. I cannot mapTo(\"%s\")",
246+
$class
247+
));
248+
}
249+
$iterator = [];
250+
$ncol = count($response['columns']);
251+
foreach ($response['values'] as $value) {
252+
$obj = new $class;
253+
for ($i=0; $i < $ncol; $i++) {
254+
$field = Utility::formatVariableName($response['columns'][$i]['name']);
255+
if ($class !== stdClass::class && !property_exists($obj, $field)) {
256+
continue;
257+
}
258+
switch($response['columns'][$i]['type']) {
259+
case 'boolean':
260+
$obj->{$field} = (bool) $value[$i];
261+
break;
262+
case 'date':
263+
$obj->{$field} = new DateTime($value[$i]);
264+
break;
265+
case 'alias':
266+
case 'text':
267+
case 'keyword':
268+
case 'ip':
269+
$obj->{$field} = (string) $value[$i];
270+
break;
271+
case 'integer':
272+
$obj->{$field} = (int) $value[$i];
273+
break;
274+
case 'long':
275+
case 'double':
276+
$obj->{$field} = (float) $value[$i];
277+
break;
278+
case 'null':
279+
$obj->{$field} = null;
280+
break;
281+
default:
282+
$obj->{$field} = $value[$i];
283+
}
284+
}
285+
$iterator[] = $obj;
286+
}
287+
return $iterator;
288+
}
227289
}

src/Utility.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,18 @@ public static function urlencode(string $url): string
4545
? urlencode($url)
4646
: rawurlencode($url);
4747
}
48+
49+
/**
50+
* Remove all the characters not valid for a PHP variable name
51+
* The valid characters are: a-z, A-Z, 0-9 and _ (underscore)
52+
* The variable name CANNOT start with a number
53+
*/
54+
public static function formatVariableName(string $var): string
55+
{
56+
// If the first character is a digit, we append the underscore
57+
if (is_int($var[0])) {
58+
$var = '_' . $var;
59+
}
60+
return preg_replace('/[^a-zA-Z0-9_]/', '', $var);
61+
}
4862
}

tests/Response/ElasticsearchTest.php

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@
1414

1515
namespace Elastic\Elasticsearch\Tests\Response;
1616

17+
use DateTime;
1718
use Elastic\Elasticsearch\Exception\ArrayAccessException;
1819
use Elastic\Elasticsearch\Exception\ClientResponseException;
1920
use Elastic\Elasticsearch\Exception\ProductCheckException;
2021
use Elastic\Elasticsearch\Exception\ServerResponseException;
2122
use Elastic\Elasticsearch\Response\Elasticsearch;
23+
use Elastic\Transport\Exception\UnknownContentTypeException;
2224
use Nyholm\Psr7\Factory\Psr17Factory;
2325
use PHPUnit\Framework\TestCase;
2426
use Psr\Http\Message\ResponseInterface;
25-
27+
use stdClass;
2628
class ElasticsearchTest extends TestCase
2729
{
2830
protected Psr17Factory $psr17Factory;
@@ -215,4 +217,84 @@ public function testWithStatusForPsr7Version1And2Compatibility()
215217
$this->elasticsearch = $this->elasticsearch->withStatus(400);
216218
$this->assertEquals(400, $this->elasticsearch->getStatusCode());
217219
}
220+
221+
public function testMapToStdClassAsDefault()
222+
{
223+
$array = [
224+
'columns' => [
225+
['name' => 'a', 'type' => 'integer'],
226+
['name' => 'b', 'type' => 'date']
227+
],
228+
'values' => [
229+
[1, '2023-10-23T12:15:03.360Z'],
230+
[3, '2023-10-23T13:55:01.543Z']
231+
]
232+
];
233+
$body = $this->psr17Factory->createStream(json_encode($array));
234+
$this->elasticsearch->setResponse($this->response200->withBody($body));
235+
236+
$iterator = $this->elasticsearch->mapTo();
237+
$this->assertIsArray($iterator);
238+
$this->assertEquals(stdClass::class, get_class($iterator[0]));
239+
$this->assertEquals(stdClass::class, get_class($iterator[1]));
240+
$this->assertEquals('integer', gettype($iterator[0]->a));
241+
$this->assertEquals(DateTime::class, get_class($iterator[0]->b));
242+
$this->assertEquals('integer', gettype($iterator[1]->a));
243+
$this->assertEquals(DateTime::class, get_class($iterator[1]->b));
244+
}
245+
246+
public function testMapToStdClass()
247+
{
248+
$array = [
249+
'columns' => [
250+
['name' => 'a', 'type' => 'integer'],
251+
['name' => 'b', 'type' => 'date']
252+
],
253+
'values' => [
254+
[1, '2023-10-23T12:15:03.360Z'],
255+
[3, '2023-10-23T13:55:01.543Z']
256+
]
257+
];
258+
$body = $this->psr17Factory->createStream(json_encode($array));
259+
$this->elasticsearch->setResponse($this->response200->withBody($body));
260+
261+
$iterator = $this->elasticsearch->mapTo(stdClass::class);
262+
$this->assertIsArray($iterator);
263+
$this->assertEquals(stdClass::class, get_class($iterator[0]));
264+
$this->assertEquals(stdClass::class, get_class($iterator[1]));
265+
}
266+
267+
public function testMapToWithoutEsqlResponseWillThrowException()
268+
{
269+
$array = ['foo' => 'bar'];
270+
$body = $this->psr17Factory->createStream(json_encode($array));
271+
$this->elasticsearch->setResponse($this->response200->withBody($body));
272+
273+
$this->expectException(UnknownContentTypeException::class);
274+
$iterator = $this->elasticsearch->mapTo();
275+
}
276+
277+
public function testMapToCustomClass()
278+
{
279+
$array = [
280+
'columns' => [
281+
['name' => 'a', 'type' => 'integer'],
282+
['name' => 'b', 'type' => 'date']
283+
],
284+
'values' => [
285+
[1, '2023-10-23T12:15:03.360Z'],
286+
[3, '2023-10-23T13:55:01.543Z']
287+
]
288+
];
289+
$body = $this->psr17Factory->createStream(json_encode($array));
290+
$this->elasticsearch->setResponse($this->response200->withBody($body));
291+
292+
$iterator = $this->elasticsearch->mapTo(TestMapClass::class);
293+
294+
$this->assertIsArray($iterator);
295+
$this->assertEquals(TestMapClass::class, get_class($iterator[0]));
296+
$this->assertEquals('integer', gettype($iterator[0]->a));
297+
$this->assertEquals(DateTime::class, get_class($iterator[0]->b));
298+
$this->assertEquals('', $iterator[0]->c);
299+
}
218300
}

tests/Response/TestMapClass.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
/**
3+
* Elasticsearch PHP Client
4+
*
5+
* @link https://github.com/elastic/elasticsearch-php
6+
* @copyright Copyright (c) Elasticsearch B.V (https://www.elastic.co)
7+
* @license https://opensource.org/licenses/MIT MIT License
8+
*
9+
* Licensed to Elasticsearch B.V under one or more agreements.
10+
* Elasticsearch B.V licenses this file to you under the MIT License.
11+
* See the LICENSE file in the project root for more information.
12+
*/
13+
declare(strict_types = 1);
14+
15+
namespace Elastic\Elasticsearch\Tests\Response;
16+
17+
use DateTime;
18+
19+
class TestMapClass
20+
{
21+
public int $a;
22+
public DateTime $b;
23+
public string $c = '';
24+
}

0 commit comments

Comments
 (0)