Skip to content
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
73 changes: 73 additions & 0 deletions Mapping/DateHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/*
* This file is part of the ONGR package.
*
* (c) NFQ Technologies UAB <info@nfq.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace ONGR\ElasticsearchBundle\Mapping;

/**
* Helps to format elasticsearch time string to interval in seconds.
*/
class DateHelper
{
/**
* Parses elasticsearch type of string into milliseconds.
*
* @param string $timeString
*
* @return int
*
* @throws \InvalidArgumentException
*/
public static function parseString($timeString)
{
$results = [];
preg_match_all('/(\d+)([a-zA-Z]+)/', $timeString, $results);
$values = $results[1];
$units = $results[2];

if (count($values) != count($units) || count($values) == 0) {
throw new \InvalidArgumentException("Invalid time string '{$timeString}'.");
}

$result = 0;
foreach ($values as $key => $value) {
$result += $value * self::charToInterval($units[$key]);
}

return $result;
}

/**
* Converts a string to time interval.
*
* @param string $value
*
* @return int
*
* @throws \InvalidArgumentException
*/
private static function charToInterval($value)
{
switch($value) {
case 'w':
return 604800000;
case 'd':
return 86400000;
case 'h':
return 3600000;
case 'm':
return 60000;
case 'ms':
return 1;
default:
throw new \InvalidArgumentException("Unknown time unit '{$value}'.");
}
}
}
31 changes: 31 additions & 0 deletions Mapping/MappingTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ class MappingTool
protected $ignoredFields = [
'type' => 'object',
'_routing' => ['required' => true],
'format' => 'dateOptionalTime',
];

/**
* @var array
*/
protected $formatFields = [
'_ttl' => 'handleTime',
];

/**
Expand Down Expand Up @@ -108,11 +116,34 @@ private function arrayFilterRecursive(array $array)
unset($array[$key]);
continue;
}

if (array_key_exists($key, $this->formatFields)) {
$array[$key] = call_user_func([$this, $this->formatFields[$key]], $array[$key]);
}

if (is_array($array[$key])) {
$array[$key] = $this->arrayFilterRecursive($array[$key]);
}
}

return $array;
}

/**
* Change time formats to fit elasticsearch.
*
* @param array $value
*
* @return array
*/
private function handleTime($value)
{
if (!isset($value['default']) || !is_string($value['default'])) {
return $value;
}

$value['default'] = DateHelper::parseString($value['default']);

return $value;
}
}
7 changes: 7 additions & 0 deletions Resources/doc/mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ class content implements DocumentInterface
`type` parameter is for type name. This parame is optional, if there will be no param set Elasticsearch bundle will create type with lovercase class name. Additional params:
* **TTL (time to live)** - `_ttl={"enabled": true, "default": "1d"}` param with which you can enable documents to have time to live and set default time interval. After time runs out document deletes itself automatically.

> You can use time units specified in [elasticsearch documentation][es-time-units].
ESB parses it if needed using [DateHelper][date-helper], e.g. for type mapping update.

`DocumentTrait` includes support with all special fields in elasticsearch document such as `_id`, `_source`, `_ttl`, `_parent` handling.
`DocumentTrait` has all parameters and setters already defined for you. Once there will be _ttl set Elasticsearch bundle will handle it automatically.

Expand Down Expand Up @@ -173,3 +176,7 @@ class content implements DocumentInterface
To define object fields the same `@ES\Property` annotations could be used. In the objects there is possibility to define other objects.

> Nested types can be defined the same way as objects, except @ES\Nested annotation must be used.


[es-time-units]:http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-ttl-field.html#_default
[date-helper]:/Mapping/DateHelper.php
200 changes: 200 additions & 0 deletions Tests/Functional/Command/TypeUpdateCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<?php

/*
* This file is part of the ONGR package.
*
* (c) NFQ Technologies UAB <info@nfq.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace ONGR\ElasticsearchBundle\Tests\Functional\Command;

use ONGR\ElasticsearchBundle\Command\IndexCreateCommand;
use ONGR\ElasticsearchBundle\Command\TypeUpdateCommand;
use ONGR\ElasticsearchBundle\ORM\Manager;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Functional tests for type update command.
*/
class TypeUpdateCommandTest extends WebTestCase
{
/**
* @var string
*/
private $documentDir;

/**
* @var Manager
*/
private $manager;

/**
* @var Application
*/
private $app;

/**
* @var string
*/
private $file;

/**
* @var ContainerInterface
*/
private $container;

/**
* {@inheritdoc}
*/
public function setUp()
{
// Only a single instance of container should be used on all commands and throughout the test.
$this->container = self::createClient()->getContainer();
$this->manager = $this->container->get('es.manager');

// Set up custom document to test mapping with.
$this->documentDir = $this->container->get('kernel')->locateResource('@ONGRTestingBundle/Document/');
$this->file = $this->documentDir . 'Article.php';

// Create index for testing.
$this->app = new Application();
$this->createIndexCommand();
}

/**
* Check if update works as expected.
*/
public function testExecute()
{
$this->assertMappingNotSet("Article mapping shouldn't be defined yet.");

copy($this->documentDir . 'documentSample.txt', $this->file);

$this->runUpdateCommand();
$this->assertMappingSet('Article mapping should be defined after update.');
}

/**
* Check if updating works with type selected.
*/
public function testExecuteType()
{
$this->assertMappingNotSet("Article mapping shouldn't be defined yet.");

copy($this->documentDir . 'documentSample.txt', $this->file);

$this->runUpdateCommand('product');
$this->assertMappingNotSet("Article mapping shouldn't be defined, type selected was `product`.");

$this->runUpdateCommand('article');
$this->assertMappingSet('Article mapping should be defined after update, type selected was `article`.');
}

/**
* Check if up to date mapping check works.
*/
public function testExecuteUpdated()
{
$this->assertStringStartsWith('Types are already up to date.', $this->runUpdateCommand());
$this->assertMappingNotSet("Article was never added, type shouldn't be added.");
}

/**
* Asserts mapping is set and correct.
*
* @param string $message
*/
protected function assertMappingSet($message)
{
$mapping = $this->manager->getConnection()->getMapping('article');
$this->assertNotNull($mapping, $message);
$expectedMapping = [
'properties' => [
'title' => [
'type' => 'string',
]
]
];
$this->assertEquals($expectedMapping, $mapping);
}

/**
* Asserts mapping isn't set.
*
* @param string $message
*/
protected function assertMappingNotSet($message)
{
$this->assertNull($this->manager->getConnection()->getMapping('article'), $message);
}

/**
* Runs update command.
*
* @param string $type
*
* @return string
*/
protected function runUpdateCommand($type = '')
{
$command = new TypeUpdateCommand();
$command->setContainer($this->container);

$this->app->add($command);
$commandToTest = $this->app->find('es:type:update');
$commandTester = new CommandTester($commandToTest);

$result = $commandTester->execute(
[
'command' => $commandToTest->getName(),
'--force' => true,
'--type' => $type,
]
);

$this->assertEquals(0, $result, "Mapping update wasn't executed successfully.");

return $commandTester->getDisplay();
}

/**
* Creates index for testing.
*
* @param string $manager
*/
protected function createIndexCommand($manager = 'default')
{
$command = new IndexCreateCommand();
$command->setContainer($this->container);

$this->app->add($command);
$command = $this->app->find('es:index:create');
$commandTester = new CommandTester($command);
$commandTester->execute(
[
'command' => $command->getName(),
'--manager' => $manager,
]
);
}

/**
* {@inheritdoc}
*/
public function tearDown()
{
try {
$this->manager->getConnection()->dropIndex();
} catch (Exception $ex) {
// Index wasn't actually created.
}
@unlink($this->file);
}
}
Loading