Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added mapTo feature, see #1398 #1399

Merged
merged 2 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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']) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a file in Elasticsearch where we can see all the classes? It would be nice to add it in a comment if we have it, in case more types are added in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = '';
}
Loading