Skip to content

Commit

Permalink
Added mapTo feature, see #1398 (#1399)
Browse files Browse the repository at this point in the history
* Added mapTo feature, see #1398

* Fixed php-http/message-factory issue and extended memory to 256M for phpstan
  • Loading branch information
ezimuel authored Mar 26, 2024
1 parent f70c97a commit acade92
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 4 deletions.
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@
"symfony/finder": "~4.0",
"nyholm/psr7": "^1.5",
"php-http/mock-client": "^1.5",
"symfony/http-client": "^5.0|^6.0",
"psr/http-factory" : "^1.0"
"symfony/http-client": "^5.0|^6.0|^7.0",
"psr/http-factory" : "^1.0",
"php-http/message-factory" : "^1.0"
},
"autoload": {
"psr-4": {
Expand All @@ -51,7 +52,7 @@
"vendor/bin/phpunit --testdox -c phpunit-integration-cloud-tests.xml"
],
"phpstan": [
"phpstan analyse src --level 2 --no-progress"
"phpstan analyse src --level 2 --no-progress --memory-limit 256M"
]
},
"config": {
Expand Down
62 changes: 62 additions & 0 deletions src/Response/Elasticsearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@
namespace Elastic\Elasticsearch\Response;

use ArrayAccess;
use DateTime;
use Elastic\Elasticsearch\Exception\ArrayAccessException;
use Elastic\Elasticsearch\Exception\ClientResponseException;
use Elastic\Elasticsearch\Exception\ServerResponseException;
use Elastic\Elasticsearch\Traits\MessageResponseTrait;
use Elastic\Elasticsearch\Traits\ProductCheckTrait;
use Elastic\Elasticsearch\Utility;
use Elastic\Transport\Exception\UnknownContentTypeException;
use Elastic\Transport\Serializer\CsvSerializer;
use Elastic\Transport\Serializer\JsonSerializer;
use Elastic\Transport\Serializer\NDJsonSerializer;
use Elastic\Transport\Serializer\XmlSerializer;
use Psr\Http\Message\ResponseInterface;
use stdClass;

/**
* Wraps a PSR-7 ResponseInterface offering helpers to deserialize the body response
Expand Down Expand Up @@ -224,4 +227,63 @@ public function offsetUnset($offset): void
{
throw new ArrayAccessException('The array is reading only');
}

/**
* Map the response body to an object of a specific class
* by default the class is the PHP standard one (stdClass)
*
* This mapping works only for ES|QL results (with columns and values)
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html
*
* @return object[]
*/
public function mapTo(string $class = stdClass::class): array
{
$response = $this->asArray();
if (!isset($response['columns']) || !isset($response['values'])) {
throw new UnknownContentTypeException(sprintf(
"The response is not a valid ES|QL result. I cannot mapTo(\"%s\")",
$class
));
}
$iterator = [];
$ncol = count($response['columns']);
foreach ($response['values'] as $value) {
$obj = new $class;
for ($i=0; $i < $ncol; $i++) {
$field = Utility::formatVariableName($response['columns'][$i]['name']);
if ($class !== stdClass::class && !property_exists($obj, $field)) {
continue;
}
switch($response['columns'][$i]['type']) {
case 'boolean':
$obj->{$field} = (bool) $value[$i];
break;
case 'date':
$obj->{$field} = new DateTime($value[$i]);
break;
case 'alias':
case 'text':
case 'keyword':
case 'ip':
$obj->{$field} = (string) $value[$i];
break;
case 'integer':
$obj->{$field} = (int) $value[$i];
break;
case 'long':
case 'double':
$obj->{$field} = (float) $value[$i];
break;
case 'null':
$obj->{$field} = null;
break;
default:
$obj->{$field} = $value[$i];
}
}
$iterator[] = $obj;
}
return $iterator;
}
}
14 changes: 14 additions & 0 deletions src/Utility.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,18 @@ public static function urlencode(string $url): string
? urlencode($url)
: rawurlencode($url);
}

/**
* Remove all the characters not valid for a PHP variable name
* The valid characters are: a-z, A-Z, 0-9 and _ (underscore)
* The variable name CANNOT start with a number
*/
public static function formatVariableName(string $var): string
{
// If the first character is a digit, we append the underscore
if (is_int($var[0])) {
$var = '_' . $var;
}
return preg_replace('/[^a-zA-Z0-9_]/', '', $var);
}
}
84 changes: 83 additions & 1 deletion tests/Response/ElasticsearchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@

namespace Elastic\Elasticsearch\Tests\Response;

use DateTime;
use Elastic\Elasticsearch\Exception\ArrayAccessException;
use Elastic\Elasticsearch\Exception\ClientResponseException;
use Elastic\Elasticsearch\Exception\ProductCheckException;
use Elastic\Elasticsearch\Exception\ServerResponseException;
use Elastic\Elasticsearch\Response\Elasticsearch;
use Elastic\Transport\Exception\UnknownContentTypeException;
use Nyholm\Psr7\Factory\Psr17Factory;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;

use stdClass;
class ElasticsearchTest extends TestCase
{
protected Psr17Factory $psr17Factory;
Expand Down Expand Up @@ -215,4 +217,84 @@ public function testWithStatusForPsr7Version1And2Compatibility()
$this->elasticsearch = $this->elasticsearch->withStatus(400);
$this->assertEquals(400, $this->elasticsearch->getStatusCode());
}

public function testMapToStdClassAsDefault()
{
$array = [
'columns' => [
['name' => 'a', 'type' => 'integer'],
['name' => 'b', 'type' => 'date']
],
'values' => [
[1, '2023-10-23T12:15:03.360Z'],
[3, '2023-10-23T13:55:01.543Z']
]
];
$body = $this->psr17Factory->createStream(json_encode($array));
$this->elasticsearch->setResponse($this->response200->withBody($body));

$iterator = $this->elasticsearch->mapTo();
$this->assertIsArray($iterator);
$this->assertEquals(stdClass::class, get_class($iterator[0]));
$this->assertEquals(stdClass::class, get_class($iterator[1]));
$this->assertEquals('integer', gettype($iterator[0]->a));
$this->assertEquals(DateTime::class, get_class($iterator[0]->b));
$this->assertEquals('integer', gettype($iterator[1]->a));
$this->assertEquals(DateTime::class, get_class($iterator[1]->b));
}

public function testMapToStdClass()
{
$array = [
'columns' => [
['name' => 'a', 'type' => 'integer'],
['name' => 'b', 'type' => 'date']
],
'values' => [
[1, '2023-10-23T12:15:03.360Z'],
[3, '2023-10-23T13:55:01.543Z']
]
];
$body = $this->psr17Factory->createStream(json_encode($array));
$this->elasticsearch->setResponse($this->response200->withBody($body));

$iterator = $this->elasticsearch->mapTo(stdClass::class);
$this->assertIsArray($iterator);
$this->assertEquals(stdClass::class, get_class($iterator[0]));
$this->assertEquals(stdClass::class, get_class($iterator[1]));
}

public function testMapToWithoutEsqlResponseWillThrowException()
{
$array = ['foo' => 'bar'];
$body = $this->psr17Factory->createStream(json_encode($array));
$this->elasticsearch->setResponse($this->response200->withBody($body));

$this->expectException(UnknownContentTypeException::class);
$iterator = $this->elasticsearch->mapTo();
}

public function testMapToCustomClass()
{
$array = [
'columns' => [
['name' => 'a', 'type' => 'integer'],
['name' => 'b', 'type' => 'date']
],
'values' => [
[1, '2023-10-23T12:15:03.360Z'],
[3, '2023-10-23T13:55:01.543Z']
]
];
$body = $this->psr17Factory->createStream(json_encode($array));
$this->elasticsearch->setResponse($this->response200->withBody($body));

$iterator = $this->elasticsearch->mapTo(TestMapClass::class);

$this->assertIsArray($iterator);
$this->assertEquals(TestMapClass::class, get_class($iterator[0]));
$this->assertEquals('integer', gettype($iterator[0]->a));
$this->assertEquals(DateTime::class, get_class($iterator[0]->b));
$this->assertEquals('', $iterator[0]->c);
}
}
24 changes: 24 additions & 0 deletions tests/Response/TestMapClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
/**
* Elasticsearch PHP Client
*
* @link https://github.com/elastic/elasticsearch-php
* @copyright Copyright (c) Elasticsearch B.V (https://www.elastic.co)
* @license https://opensource.org/licenses/MIT MIT License
*
* Licensed to Elasticsearch B.V under one or more agreements.
* Elasticsearch B.V licenses this file to you under the MIT License.
* See the LICENSE file in the project root for more information.
*/
declare(strict_types = 1);

namespace Elastic\Elasticsearch\Tests\Response;

use DateTime;

class TestMapClass
{
public int $a;
public DateTime $b;
public string $c = '';
}

0 comments on commit acade92

Please sign in to comment.