Merge branch 'master' into pr/725

Conflicts:
	Doctrine/AbstractProvider.php
This commit is contained in:
Tim Nagel 2015-03-11 15:47:03 +11:00
commit 3d69c08b5a
50 changed files with 924 additions and 209 deletions

View file

@ -1,7 +1,12 @@
language: php
cache:
directories:
- $HOME/.composer/cache
php:
- 5.3
- 5.4
- 5.5
- 5.6
@ -11,8 +16,6 @@ matrix:
env: SYMFONY_VERSION='2.3.*'
- php: 5.5
env: SYMFONY_VERSION='2.5.*'
- php: 5.5
env: SYMFONY_VERSION='dev-master'
before_script:
- /usr/share/elasticsearch/bin/elasticsearch -v
@ -22,7 +25,8 @@ before_script:
- sh -c 'if [ "$SYMFONY_VERSION" != "" ]; then composer require --dev --no-update symfony/symfony=$SYMFONY_VERSION; fi;'
- composer install --dev --prefer-source
script: vendor/bin/phpunit --coverage-clover=coverage.clover
script:
- vendor/bin/phpunit --coverage-clover=coverage.clover
services:
- elasticsearch

View file

@ -12,6 +12,24 @@ https://github.com/FriendsOfSymfony/FOSElasticaBundle/compare/v3.0.0...v3.0.1
To generate a changelog summary since the last version, run
`git log --no-merges --oneline v3.0.0...3.0.x`
* 3.0.8 (2014-01-31)
* Fixed handling of empty indexes #760
* Added support for `connectionStrategy` Elastica configuration #732
* Allow Elastica 1.4
* 3.0.7 (2015-01-21)
* Fixed the indexing of parent/child relations, broken since 3.0 #774
* Fixed multi_field properties not being normalised #769
* 3.0.6 (2015-01-04)
* Removed unused public image asset for the web development toolbar #742
* Fixed is_indexable_callback BC code to support array notation #761
* Fixed debug_logger for type providers #724
* Clean the OM if we filter away the entire batch #737
* 3.0.0-ALPHA6
* Moved `is_indexable_callback` from the listener properties to a type property called

View file

@ -1,4 +1,4 @@
CHANGELOG for 3.0.x
CHANGELOG for 3.1.x
===================
This changelog references the relevant changes (bug and security fixes) done
@ -9,8 +9,27 @@ https://github.com/FriendsOfSymfony/FOSElasticaBundle/commit/XXX where XXX is
the commit hash. To get the diff between two versions, go to
https://github.com/FriendsOfSymfony/FOSElasticaBundle/compare/v3.0.4...v3.1.0
* 3.1.0
* 3.1.0 (Unreleased)
* BC BREAK: `DoctrineListener#scheduleForDeletion` access changed to private.
* BC BREAK: `ObjectPersisterInterface` gains the method `handlesObject` that
returns a boolean value if it will handle a given object or not.
* BC BREAK: `Doctrine\Listener#scheduleForDeletion` access changed to private.
* BC BREAK: `ObjectPersisterInterface` gains the method `handlesObject` that
returns a boolean value if it will handle a given object or not.
* BC BREAK: Removed `Doctrine\Listener#getSubscribedEvents`. The container
configuration now configures tags with the methods to call to avoid loading
this class on every request where doctrine is active. #729
* BC BREAK: Added methods for retrieving aggregations when paginating results.
The `PaginationAdapterInterface` has a new method, `getAggregations`. #726
* Added ability to configure `date_detection`, `numeric_detection` and
`dynamic_date_formats` for types. #753
* New event `POST_TRANSFORM` which allows developers to add custom properties to
Elastica Documents for indexing.
* When available, the `fos:elastica:populate` command will now use the
ProgressBar helper instead of outputting strings. You can use verbosity
controls on the command to output additional information like memory
usage, runtime and estimated time.
* Added new option `property_path` to a type property definition to allow
customisation of the property path used to retrieve data from objects.
Setting `property_path` to `false` will configure the Transformer to ignore
that property while transforming. Combined with the above POST_TRANSFORM event
developers can now create calculated dynamic properties on Elastica documents
for indexing. #794

View file

@ -11,6 +11,7 @@ use FOS\ElasticaBundle\IndexManager;
use FOS\ElasticaBundle\Provider\ProviderRegistry;
use FOS\ElasticaBundle\Resetter;
use FOS\ElasticaBundle\Provider\ProviderInterface;
use Symfony\Component\Console\Helper\ProgressBar;
/**
* Populate the search index
@ -99,6 +100,82 @@ class PopulateCommand extends ContainerAwareCommand
}
}
/**
* @param ProviderInterface $provider
* @param OutputInterface $output
* @param string $input
* @param string $type
* @param array $options
*/
private function doPopulateType(ProviderInterface $provider, OutputInterface $output, $input, $type, $options)
{
$loggerClosure = $this->getLoggerClosure($output, $input, $type);
$provider->populate($loggerClosure, $options);
}
/**
* Builds a loggerClosure to be called from inside the Provider to update the command
* line.
*
* @param OutputInterface $output
* @param string $index
* @param string $type
* @return callable
*/
private function getLoggerClosure(OutputInterface $output, $index, $type)
{
if (!class_exists('Symfony\Component\Console\Helper\ProgressBar')) {
$lastStep = null;
$current = 0;
return function ($increment, $totalObjects) use ($output, $index, $type, &$lastStep, &$current) {
if ($current + $increment > $totalObjects) {
$increment = $totalObjects - $current;
}
$currentTime = microtime(true);
$timeDifference = $currentTime - $lastStep;
$objectsPerSecond = $lastStep ? ($increment / $timeDifference) : $increment;
$lastStep = $currentTime;
$current += $increment;
$percent = 100 * $current / $totalObjects;
$output->writeln(sprintf(
'<info>Populating</info> <comment>%s/%s</comment> %0.1f%% (%d/%d), %d objects/s (RAM: current=%uMo peak=%uMo)',
$index,
$type,
$percent,
$current,
$totalObjects,
$objectsPerSecond,
round(memory_get_usage() / (1024 * 1024)),
round(memory_get_peak_usage() / (1024 * 1024))
));
};
}
ProgressBar::setFormatDefinition('normal', " %current%/%max% [%bar%] %percent:3s%%\n%message%");
ProgressBar::setFormatDefinition('verbose', " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%\n%message%");
ProgressBar::setFormatDefinition('very_verbose', " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%\n%message%");
ProgressBar::setFormatDefinition('debug', " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%\n%message%");
$progress = null;
return function ($increment, $totalObjects) use (&$progress, $output, $index, $type) {
if (null === $progress) {
$progress = new ProgressBar($output, $totalObjects);
$progress->start();
}
$progress->setMessage(sprintf('<info>Populating</info> <comment>%s/%s</comment>', $index, $type));
$progress->advance($increment);
if ($progress->getProgressPercent() >= 1.0) {
$progress->finish();
}
};
}
/**
* Recreates an index, populates its types, and refreshes the index.
*
@ -118,16 +195,10 @@ class PopulateCommand extends ContainerAwareCommand
$providers = $this->providerRegistry->getIndexProviders($index);
foreach ($providers as $type => $provider) {
$loggerClosure = function($message) use ($output, $index, $type) {
$output->writeln(sprintf('<info>Populating</info> %s/%s, %s', $index, $type, $message));
};
$provider->populate($loggerClosure, $options);
$this->doPopulateType($provider, $output, $index, $type, $options);
}
$output->writeln(sprintf('<info>Refreshing</info> <comment>%s</comment>', $index));
$this->resetter->postPopulate($index);
$this->indexManager->getIndex($index)->refresh();
$this->refreshIndex($output, $index);
}
/**
@ -146,12 +217,24 @@ class PopulateCommand extends ContainerAwareCommand
$this->resetter->resetIndexType($index, $type);
}
$loggerClosure = function($message) use ($output, $index, $type) {
$output->writeln(sprintf('<info>Populating</info> %s/%s, %s', $index, $type, $message));
};
$provider = $this->providerRegistry->getProvider($index, $type);
$provider->populate($loggerClosure, $options);
$this->doPopulateType($provider, $output, $index, $type, $options);
$this->refreshIndex($output, $index, false);
}
/**
* Refreshes an index.
*
* @param OutputInterface $output
* @param string $index
* @param bool $postPopulate
*/
private function refreshIndex(OutputInterface $output, $index, $postPopulate = true)
{
if ($postPopulate) {
$this->resetter->postPopulate($index);
}
$output->writeln(sprintf('<info>Refreshing</info> <comment>%s</comment>', $index));
$this->indexManager->getIndex($index)->refresh();

View file

@ -40,16 +40,7 @@ class ContainerSource implements SourceInterface
{
$indexes = array();
foreach ($this->configArray as $config) {
$types = array();
foreach ($config['types'] as $typeConfig) {
$types[$typeConfig['name']] = new TypeConfig(
$typeConfig['name'],
$typeConfig['mapping'],
$typeConfig['config']
);
// TODO: handle prototypes..
}
$types = $this->getTypes($config);
$index = new IndexConfig($config['name'], $types, array(
'elasticSearchName' => $config['elasticsearch_name'],
'settings' => $config['settings'],
@ -61,4 +52,28 @@ class ContainerSource implements SourceInterface
return $indexes;
}
/**
* Builds TypeConfig objects for each type.
*
* @param array $config
* @return array
*/
protected function getTypes($config)
{
$types = array();
if (isset($config['types'])) {
foreach ($config['types'] as $typeConfig) {
$types[$typeConfig['name']] = new TypeConfig(
$typeConfig['name'],
$typeConfig['mapping'],
$typeConfig['config']
);
// TODO: handle prototypes..
}
}
return $types;
}
}

View file

@ -35,6 +35,22 @@ class TypeConfig
$this->name = $name;
}
/**
* @return bool|null
*/
public function getDateDetection()
{
return $this->getConfig('date_detection');
}
/**
* @return array
*/
public function getDynamicDateFormats()
{
return $this->getConfig('dynamic_date_formats');
}
/**
* @return string|null
*/
@ -61,6 +77,14 @@ class TypeConfig
null;
}
/**
* @return bool|null
*/
public function getNumericDetection()
{
return $this->getConfig('numeric_detection');
}
/**
* @return string
*/

View file

@ -84,6 +84,16 @@ class Configuration implements ConfigurationInterface
return $v;
})
->end()
// Elastica names its properties with camel case, support both
->beforeNormalization()
->ifTrue(function ($v) { return isset($v['connection_strategy']); })
->then(function ($v) {
$v['connectionStrategy'] = $v['connection_strategy'];
unset($v['connection_strategy']);
return $v;
})
->end()
// If there is no connections array key defined, assume a single connection.
->beforeNormalization()
->ifTrue(function ($v) { return is_array($v) && !array_key_exists('connections', $v); })
@ -124,6 +134,7 @@ class Configuration implements ConfigurationInterface
->end()
->scalarNode('timeout')->end()
->scalarNode('headers')->end()
->scalarNode('connectionStrategy')->defaultValue('Simple')->end()
->end()
->end()
->end()
@ -181,6 +192,10 @@ class Configuration implements ConfigurationInterface
->useAttributeAsKey('name')
->prototype('array')
->treatNullLike(array())
->beforeNormalization()
->ifNull()
->thenEmptyArray()
->end()
// BC - Renaming 'mappings' node to 'properties'
->beforeNormalization()
->ifTrue(function($v) { return array_key_exists('mappings', $v); })
@ -199,7 +214,17 @@ class Configuration implements ConfigurationInterface
isset($v['persistence']['listener']['is_indexable_callback']);
})
->then(function ($v) {
$v['indexable_callback'] = $v['persistence']['listener']['is_indexable_callback'];
$callback = $v['persistence']['listener']['is_indexable_callback'];
if (is_array($callback)) {
list($class) = $callback + array(null);
if ($class[0] !== '@' && is_string($class) && !class_exists($class)) {
$callback[0] = '@'.$class;
}
}
$v['indexable_callback'] = $callback;
unset($v['persistence']['listener']['is_indexable_callback']);
return $v;
@ -225,7 +250,10 @@ class Configuration implements ConfigurationInterface
})
->end()
->children()
->booleanNode('date_detection')->end()
->arrayNode('dynamic_date_formats')->prototype('scalar')->end()->end()
->scalarNode('index_analyzer')->end()
->booleanNode('numeric_detection')->end()
->scalarNode('search_analyzer')->end()
->variableNode('indexable_callback')->end()
->append($this->getPersistenceNode())
@ -480,9 +508,13 @@ class Configuration implements ConfigurationInterface
->scalarNode('identifier')->defaultValue('id')->end()
->arrayNode('provider')
->children()
->scalarNode('query_builder_method')->defaultValue('createQueryBuilder')->end()
->scalarNode('batch_size')->defaultValue(100)->end()
->scalarNode('clear_object_manager')->defaultTrue()->end()
->scalarNode('debug_logging')
->defaultValue($this->debug)
->treatNullLike(true)
->end()
->scalarNode('query_builder_method')->defaultValue('createQueryBuilder')->end()
->scalarNode('service')->end()
->end()
->end()

View file

@ -241,6 +241,9 @@ class FOSElasticaExtension extends Extension
'serializer',
'index_analyzer',
'search_analyzer',
'date_detection',
'dynamic_date_formats',
'numeric_detection',
) as $field) {
$typeConfig['config'][$field] = array_key_exists($field, $type) ?
$type[$field] :
@ -393,7 +396,12 @@ class FOSElasticaExtension extends Extension
$arguments[] = array(new Reference($callbackId), 'serialize');
} else {
$abstractId = 'fos_elastica.object_persister';
$arguments[] = $this->indexConfigs[$indexName]['types'][$typeName]['mapping']['properties'];
$mapping = $this->indexConfigs[$indexName]['types'][$typeName]['mapping'];
$argument = $mapping['properties'];
if(isset($mapping['_parent'])){
$argument['_parent'] = $mapping['_parent'];
}
$arguments[] = $argument;
}
$serviceId = sprintf('fos_elastica.object_persister.%s.%s', $indexName, $typeName);
@ -464,19 +472,29 @@ class FOSElasticaExtension extends Extension
$listenerId = sprintf('fos_elastica.listener.%s.%s', $indexName, $typeName);
$listenerDef = new DefinitionDecorator($abstractListenerId);
$listenerDef->replaceArgument(0, new Reference($objectPersisterId));
$listenerDef->replaceArgument(1, $this->getDoctrineEvents($typeConfig));
$listenerDef->replaceArgument(3, array(
$listenerDef->replaceArgument(2, array(
'identifier' => $typeConfig['identifier'],
'indexName' => $indexName,
'typeName' => $typeName,
));
if ($typeConfig['listener']['logger']) {
$listenerDef->replaceArgument(4, new Reference($typeConfig['listener']['logger']));
$listenerDef->replaceArgument(3, new Reference($typeConfig['listener']['logger']));
}
$tagName = null;
switch ($typeConfig['driver']) {
case 'orm': $listenerDef->addTag('doctrine.event_subscriber'); break;
case 'mongodb': $listenerDef->addTag('doctrine_mongodb.odm.event_subscriber'); break;
case 'orm':
$tagName = 'doctrine.event_listener';
break;
case 'mongodb':
$tagName = 'doctrine_mongodb.odm.event_listener';
break;
}
if ($tagName) {
foreach ($this->getDoctrineEvents($typeConfig) as $event) {
$listenerDef->addTag($tagName, array('event' => $event));
}
}
$container->setDefinition($listenerId, $listenerDef);

View file

@ -43,7 +43,7 @@ abstract class AbstractProvider extends BaseAbstractProvider
}
/**
* @see FOS\ElasticaBundle\Provider\ProviderInterface::populate()
* {@inheritDoc}
*/
public function populate(\Closure $loggerClosure = null, array $options = array())
{
@ -58,49 +58,22 @@ abstract class AbstractProvider extends BaseAbstractProvider
$batchSize = isset($options['batch-size']) ? intval($options['batch-size']) : $this->options['batch_size'];
$ignoreErrors = isset($options['ignore-errors']) ? $options['ignore-errors'] : $this->options['ignore_errors'];
$manager = $this->managerRegistry->getManagerForClass($this->objectClass);
$objects = array();
for (; $offset < $nbObjects; $offset += $batchSize) {
if ($loggerClosure) {
$stepStartTime = microtime(true);
}
if ($this->sliceFetcher) {
$identifierFieldNames = $manager
->getClassMetadata($this->objectClass)
->getIdentifierFieldNames();
$objects = $this->sliceFetcher->fetch(
$queryBuilder,
$batchSize,
$offset,
$objects,
$identifierFieldNames
);
} else {
$objects = $this->fetchSlice($queryBuilder, $batchSize, $offset);
}
if ($loggerClosure) {
$stepNbObjects = count($objects);
}
$objects = $this->getSlice($queryBuilder, $batchSize, $offset, $objects);
$objects = array_filter($objects, array($this, 'isObjectIndexable'));
if (!$objects) {
if ($loggerClosure) {
$loggerClosure('<info>Entire batch was filtered away, skipping...</info>');
}
continue;
}
if (!$ignoreErrors) {
$this->objectPersister->insertMany($objects);
} else {
try {
if ($objects) {
if (!$ignoreErrors) {
$this->objectPersister->insertMany($objects);
} catch(BulkResponseException $e) {
if ($loggerClosure) {
$loggerClosure(sprintf('<error>%s</error>',$e->getMessage()));
} else {
try {
$this->objectPersister->insertMany($objects);
} catch(BulkResponseException $e) {
if ($loggerClosure) {
$loggerClosure($batchSize, $nbObjects, sprintf('<error>%s</error>',$e->getMessage()));
}
}
}
}
@ -112,11 +85,7 @@ abstract class AbstractProvider extends BaseAbstractProvider
usleep($sleep);
if ($loggerClosure) {
$stepCount = $stepNbObjects + $offset;
$percentComplete = 100 * $stepCount / $nbObjects;
$timeDifference = microtime(true) - $stepStartTime;
$objectsPerSecond = $timeDifference ? ($stepNbObjects / $timeDifference) : $stepNbObjects;
$loggerClosure(sprintf('%0.1f%% (%d/%d), %d objects/s %s', $percentComplete, $stepCount, $nbObjects, $objectsPerSecond, $this->getMemoryUsage()));
$loggerClosure($batchSize, $nbObjects);
}
}
@ -125,6 +94,36 @@ abstract class AbstractProvider extends BaseAbstractProvider
}
}
/**
* If this Provider has a SliceFetcher defined, we use it instead of falling back to
* the fetchSlice methods defined in the ORM/MongoDB subclasses.
*
* @param $queryBuilder
* @param int $limit
* @param int $offset
* @param array $lastSlice
* @return array
*/
protected function getSlice($queryBuilder, $limit, $offset, $lastSlice)
{
if (!$this->sliceFetcher) {
return $this->fetchSlice($queryBuilder, $limit, $offset);
}
$manager = $this->managerRegistry->getManagerForClass($this->objectClass);
$identifierFieldNames = $manager
->getClassMetadata($this->objectClass)
->getIdentifierFieldNames();
return $this->sliceFetcher->fetch(
$queryBuilder,
$limit,
$offset,
$lastSlice,
$identifierFieldNames
);
}
/**
* Counts objects that would be indexed using the query builder.
*

View file

@ -2,7 +2,6 @@
namespace FOS\ElasticaBundle\Doctrine;
use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use FOS\ElasticaBundle\Persister\ObjectPersisterInterface;
use FOS\ElasticaBundle\Persister\ObjectPersister;
@ -14,7 +13,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
* Automatically update ElasticSearch based on changes to the Doctrine source
* data. One listener is generated for each Doctrine entity / ElasticSearch type.
*/
class Listener implements EventSubscriber
class Listener
{
/**
* Object persister
@ -23,13 +22,6 @@ class Listener implements EventSubscriber
*/
protected $objectPersister;
/**
* List of subscribed events
*
* @var array
*/
protected $events;
/**
* Configuration for the listener
*
@ -74,14 +66,12 @@ class Listener implements EventSubscriber
* Constructor.
*
* @param ObjectPersisterInterface $objectPersister
* @param array $events
* @param IndexableInterface $indexable
* @param array $config
* @param null $logger
*/
public function __construct(
ObjectPersisterInterface $objectPersister,
array $events,
IndexableInterface $indexable,
array $config = array(),
$logger = null
@ -89,7 +79,6 @@ class Listener implements EventSubscriber
$this->config = array_merge(array(
'identifier' => 'id',
), $config);
$this->events = $events;
$this->indexable = $indexable;
$this->objectPersister = $objectPersister;
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
@ -99,14 +88,6 @@ class Listener implements EventSubscriber
}
}
/**
* @see Doctrine\Common\EventSubscriber::getSubscribedEvents()
*/
public function getSubscribedEvents()
{
return $this->events;
}
/**
* Looks for new objects that should be indexed.
*

78
Event/TransformEvent.php Normal file
View file

@ -0,0 +1,78 @@
<?php
/**
* This file is part of the FOSElasticaBundle project.
*
* (c) Infinite Networks Pty Ltd <http://www.infinite.net.au>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\ElasticaBundle\Event;
use Symfony\Component\EventDispatcher\Event;
class TransformEvent extends Event
{
const POST_TRANSFORM = 'fos_elastica.post_transform';
/**
* @var mixed
*/
private $document;
/**
* @var array
*/
private $fields;
/**
* @var mixed
*/
private $object;
/**
* @param mixed $document
* @param array $fields
* @param mixed $object
*/
public function __construct($document, array $fields, $object)
{
$this->document = $document;
$this->fields = $fields;
$this->object = $object;
}
/**
* @return mixed
*/
public function getDocument()
{
return $this->document;
}
/**
* @return array
*/
public function getFields()
{
return $this->fields;
}
/**
* @return mixed
*/
public function getObject()
{
return $this->object;
}
/**
* @param mixed $document
*/
public function setDocument($document)
{
$this->document = $document;
}
}

View file

@ -58,13 +58,19 @@ class MappingBuilder
*/
public function buildTypeMapping(TypeConfig $typeConfig)
{
$mapping = array_merge($typeConfig->getMapping(), array(
// 'date_detection' => true,
// 'dynamic_date_formats' => array()
// 'dynamic_templates' => $typeConfig->getDynamicTemplates(),
// 'numeric_detection' => false,
// 'properties' => array(),
));
$mapping = $typeConfig->getMapping();
if (null !== $typeConfig->getDynamicDateFormats()) {
$mapping['dynamic_date_formats'] = $typeConfig->getDynamicDateFormats();
}
if (null !== $typeConfig->getDateDetection()) {
$mapping['date_detection'] = $typeConfig->getDateDetection();
}
if (null !== $typeConfig->getNumericDetection()) {
$mapping['numeric_detection'] = $typeConfig->getNumericDetection();
}
if ($typeConfig->getIndexAnalyzer()) {
$mapping['index_analyzer'] = $typeConfig->getIndexAnalyzer();
@ -104,9 +110,14 @@ class MappingBuilder
private function fixProperties(&$properties)
{
foreach ($properties as $name => &$property) {
unset($property['property_path']);
if (!isset($property['type'])) {
$property['type'] = 'string';
}
if ($property['type'] == 'multi_field' && isset($property['fields'])) {
$this->fixProperties($property['fields']);
}
if (isset($property['properties'])) {
$this->fixProperties($property['properties']);
}

View file

@ -42,6 +42,9 @@ class Resetter
/**
* Deletes and recreates all indexes
*
* @param bool $populating
* @param bool $force
*/
public function resetAllIndexes($populating = false, $force = false)
{

View file

@ -39,6 +39,18 @@ class FantaPaginatorAdapter implements AdapterInterface
{
return $this->adapter->getFacets();
}
/**
* Returns Aggregations
*
* @return mixed
*
* @api
*/
public function getAggregations()
{
return $this->adapter->getAggregations();
}
/**
* Returns a slice of the results.

View file

@ -31,4 +31,11 @@ interface PaginatorAdapterInterface
* @return mixed
*/
function getFacets();
/**
* Returns Aggregations
*
* @return mixed
*/
function getAggregations();
}

View file

@ -28,4 +28,11 @@ interface PartialResultsInterface
* @return array
*/
function getFacets();
}
/**
* Returns the aggregations
*
* @return array
*/
function getAggregations();
}

View file

@ -36,6 +36,11 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface
* @var array for the facets
*/
private $facets;
/**
* @var array for the aggregations
*/
private $aggregations;
/**
* @see PaginatorAdapterInterface::__construct
@ -82,6 +87,7 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface
$resultSet = $this->searchable->search($query, $this->options);
$this->totalHits = $resultSet->getTotalHits();
$this->facets = $resultSet->getFacets();
$this->aggregations = $resultSet->getAggregations();
return $resultSet;
}
@ -100,15 +106,18 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface
/**
* Returns the number of results.
*
* @param boolean $genuineTotal make the function return the `hits.total`
* value of the search result in all cases, instead of limiting it to the
* `size` request parameter.
* @return integer The number of results.
*/
public function getTotalHits()
public function getTotalHits($genuineTotal = false)
{
if ( ! isset($this->totalHits)) {
$this->totalHits = $this->searchable->search($this->query)->getTotalHits();
}
return $this->query->hasParam('size')
return $this->query->hasParam('size') && !$genuineTotal
? min($this->totalHits, (integer) $this->query->getParam('size'))
: $this->totalHits;
}
@ -126,6 +135,19 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface
return $this->facets;
}
/**
* Returns Aggregations
*
* @return mixed
*/
public function getAggregations() {
if (!isset($this->aggregations)) {
$this->aggregations = $this->searchable->search($this->query)->getAggregations();
}
return $this->aggregations;
}
/**
* Returns the Query

View file

@ -49,4 +49,16 @@ class RawPartialResults implements PartialResultsInterface
return null;
}
}
/**
* {@inheritDoc}
*/
public function getAggregations()
{
if ($this->resultSet->hasAggregations()) {
return $this->resultSet->getAggregations();
}
return null;
}
}

View file

@ -12,7 +12,7 @@ use FOS\ElasticaBundle\Provider\AbstractProvider;
class Provider extends AbstractProvider
{
/**
* @see FOS\ElasticaBundle\Provider\ProviderInterface::populate()
* {@inheritDoc}
*/
public function populate(\Closure $loggerClosure = null, array $options = array())
{
@ -23,34 +23,21 @@ class Provider extends AbstractProvider
$batchSize = isset($options['batch-size']) ? intval($options['batch-size']) : $this->options['batch_size'];
for (; $offset < $nbObjects; $offset += $batchSize) {
if ($loggerClosure) {
$stepStartTime = microtime(true);
}
$objects = $queryClass::create()
->limit($batchSize)
->offset($offset)
->find()
->getArrayCopy();
if ($loggerClosure) {
$stepNbObjects = count($objects);
}
$objects = array_filter($objects, array($this, 'isObjectIndexable'));
if (!$objects) {
$loggerClosure('<info>Entire batch was filtered away, skipping...</info>');
continue;
if ($objects) {
$this->objectPersister->insertMany($objects);
}
$this->objectPersister->insertMany($objects);
usleep($sleep);
if ($loggerClosure) {
$stepCount = $stepNbObjects + $offset;
$percentComplete = 100 * $stepCount / $nbObjects;
$objectsPerSecond = $stepNbObjects / (microtime(true) - $stepStartTime);
$loggerClosure(sprintf('%0.1f%% (%d/%d), %d objects/s %s', $percentComplete, $stepCount, $nbObjects, $objectsPerSecond, $this->getMemoryUsage()));
$loggerClosure($batchSize, $nbObjects);
}
}
}

View file

@ -70,6 +70,7 @@ abstract class AbstractProvider implements ProviderInterface
/**
* Get string with RAM usage information (current and peak)
*
* @deprecated To be removed in 4.0
* @return string
*/
protected function getMemoryUsage()

View file

@ -27,7 +27,6 @@
<service id="fos_elastica.listener.prototype.mongodb" class="%fos_elastica.listener.prototype.mongodb.class%" public="false" abstract="true">
<argument /> <!-- object persister -->
<argument type="collection" /> <!-- events -->
<argument type="service" id="fos_elastica.indexable" />
<argument type="collection" /> <!-- configuration -->
<argument /> <!-- logger -->

View file

@ -27,7 +27,6 @@
<service id="fos_elastica.listener.prototype.orm" class="%fos_elastica.listener.prototype.orm.class%" public="false" abstract="true">
<argument /> <!-- object persister -->
<argument type="collection" /> <!-- events -->
<argument type="service" id="fos_elastica.indexable" />
<argument type="collection" /> <!-- configuration -->
<argument on-invalid="ignore" /> <!-- logger -->

View file

@ -12,7 +12,8 @@
<services>
<service id="fos_elastica.model_to_elastica_transformer" class="%fos_elastica.model_to_elastica_transformer.class%" public="false" abstract="true">
<argument type="collection" /> <!-- options -->
<argument type="collection" /> <!-- options -->
<argument type="service" id="event_dispatcher" /> <!-- options -->
<call method="setPropertyAccessor">
<argument type="service" id="fos_elastica.property_accessor" />
</call>

View file

@ -0,0 +1,33 @@
##### Custom Properties
Since FOSElasticaBundle 3.1.0, we now dispatch an event for each transformation of an
object into an Elastica document which allows you to set custom properties on the Elastica
document for indexing.
Set up an event listener or subscriber for
`FOS\ElasticaBundle\Event\TransformEvent::POST_TRANSFORM` to be able to inject your own
parameters.
```php
class CustomPropertyListener implements EventSubscriberInterface
{
private $anotherService;
// ...
public function addCustomProperty(TransformEvent $event)
{
$document = $event->getDocument();
$custom = $this->anotherService->calculateCustom($event->getObject());
$document->set('custom', $custom);
}
public static function getSubscribedEvents()
{
return array(
TransformEvent::POST_TRANSFORM => 'addCustomProperty',
);
}
}
```

View file

@ -4,7 +4,7 @@ As well as the default repository you can create a custom repository for an enti
methods for particular searches. These need to extend `FOS\ElasticaBundle\Repository` to have
access to the finder:
```
```php
<?php
namespace Acme\ElasticaBundle\SearchRepository;
@ -23,37 +23,41 @@ class UserRepository extends Repository
To use the custom repository specify it in the mapping for the entity:
fos_elastica:
clients:
default: { host: localhost, port: 9200 }
indexes:
website:
client: default
types:
user:
mappings:
# your mappings
persistence:
driver: orm
model: Application\UserBundle\Entity\User
provider: ~
finder: ~
repository: Acme\ElasticaBundle\SearchRepository\UserRepository
```yaml
fos_elastica:
clients:
default: { host: localhost, port: 9200 }
indexes:
website:
client: default
types:
user:
mappings:
# your mappings
persistence:
driver: orm
model: Application\UserBundle\Entity\User
provider: ~
finder: ~
repository: Acme\ElasticaBundle\SearchRepository\UserRepository
```
Then the custom queries will be available when using the repository returned from the manager:
/** var FOS\ElasticaBundle\Manager\RepositoryManager */
$repositoryManager = $container->get('fos_elastica.manager');
```php
/** var FOS\ElasticaBundle\Manager\RepositoryManager */
$repositoryManager = $container->get('fos_elastica.manager');
/** var FOS\ElasticaBundle\Repository */
$repository = $repositoryManager->getRepository('UserBundle:User');
/** var FOS\ElasticaBundle\Repository */
$repository = $repositoryManager->getRepository('UserBundle:User');
/** var array of Acme\UserBundle\Entity\User */
$users = $repository->findWithCustomQuery('bob');
/** var array of Acme\UserBundle\Entity\User */
$users = $repository->findWithCustomQuery('bob');
```
Alternatively you can specify the custom repository using an annotation in the entity:
```
```php
<?php
namespace Application\UserBundle\Entity;
@ -69,4 +73,4 @@ class User
//---
}
```
```

View file

@ -8,7 +8,7 @@ index and type for which the service will provide.
# app/config/config.yml
services:
acme.search_provider.user:
class: Acme\UserBundle\Search\UserProvider
class: Acme\UserBundle\Provider\UserProvider
arguments:
- @fos_elastica.index.website.user
tags:

View file

@ -11,6 +11,11 @@ fos_elastica:
connections:
- url: http://es1.example.net:9200
- url: http://es2.example.net:9200
connection_strategy: RoundRobin
```
Elastica allows for definition of different connection strategies and by default
supports `RoundRobin` and `Simple`. You can see definitions for these strategies
in the `Elastica\Connection\Strategy` namespace.
For more information on Elastica clustering see http://elastica.io/getting-started/installation.html#section-connect-cluster

View file

@ -23,7 +23,7 @@ namespace Acme\ElasticaBundle;
use Elastica\Exception\ExceptionInterface;
use Elastica\Request;
use Elastica\Response;
use FOS\ElasticaBundle\Client as BaseClient;
use FOS\ElasticaBundle\Elastica\Client as BaseClient;
class Client extends BaseClient
{

View file

@ -13,6 +13,7 @@ Cookbook Entries
----------------
* [Aliased Indexes](cookbook/aliased-indexes.md)
* [Custom Indexed Properties](cookbook/custom-properties.md)
* [Custom Repositories](cookbook/custom-repositories.md)
* [HTTP Headers for Elastica](cookbook/elastica-client-http-headers.md)
* Performance - [Logging](cookbook/logging.md)

View file

@ -1,40 +1,50 @@
Step 1: Setting up the bundle
=============================
A) Install FOSElasticaBundle
----------------------------
A: Download the Bundle
----------------------
FOSElasticaBundle is installed using [Composer](https://getcomposer.org).
Open a command console, enter your project directory and execute the
following command to download the latest stable version of this bundle:
```bash
$ php composer.phar require friendsofsymfony/elastica-bundle "~3.0"
$ composer require friendsofsymfony/elastica-bundle "~3.0"
```
This command requires you to have Composer installed globally, as explained
in the [installation chapter](https://getcomposer.org/doc/00-intro.md)
of the Composer documentation.
### Elasticsearch
Instructions for installing and deploying Elasticsearch may be found
[here](http://www.elasticsearch.org/guide/reference/setup/installation/).
Instructions for installing and deploying Elasticsearch may be found [here](http://www.elasticsearch.org/guide/reference/setup/installation/).
Step 2: Enable the Bundle
-------------------------
B) Enable FOSElasticaBundle
---------------------------
Enable FOSElasticaBundle in your AppKernel:
Then, enable the bundle by adding the following line in the `app/AppKernel.php`
file of your project:
```php
<?php
// app/AppKernel.php
public function registerBundles()
// ...
class AppKernel extends Kernel
{
$bundles = array(
public function registerBundles()
{
$bundles = array(
// ...
new FOS\ElasticaBundle\FOSElasticaBundle(),
);
// ...
new FOS\ElasticaBundle\FOSElasticaBundle(),
);
}
}
```
C) Basic Bundle Configuration
C: Basic Bundle Configuration
-----------------------------
The basic minimal configuration for FOSElasticaBundle is one client with one Elasticsearch
@ -48,27 +58,30 @@ fos_elastica:
clients:
default: { host: localhost, port: 9200 }
indexes:
search: ~
app: ~
```
In this example, an Elastica index (an instance of `Elastica\Index`) is available as a
service with the key `fos_elastica.index.search`.
service with the key `fos_elastica.index.app`.
If the Elasticsearch index name needs to be different to the service name in your
application, for example, renaming the search index based on different environments.
You may want the index `app` to be named something else on ElasticSearch depending on
if your application is running in a different env or other conditions that suit your
application. To set your customer index to a name that depends on the environment of your
Symfony application, use the example below:
```yaml
#app/config/config.yml
fos_elastica:
indexes:
search:
index_name: search_dev
app:
index_name: app_%kernel.env%
```
In this case, the service `fos_elastica.index.search` will be using an Elasticsearch
index of search_dev.
In this case, the service `fos_elastica.index.app` will relate to an ElasticSearch index
that varies depending on your kernel's environment. For example, in dev it will relate to
`app_dev`.
D) Defining index types
D: Defining index types
-----------------------
By default, FOSElasticaBundle requires each type that is to be indexed to be mapped.
@ -81,7 +94,7 @@ will end up being indexed.
```yaml
fos_elastica:
indexes:
search:
app:
types:
user:
mappings:
@ -92,7 +105,7 @@ fos_elastica:
```
Each defined type is made available as a service, and in this case the service key is
`fos_elastica.index.search.user` and is an instance of `Elastica\Type`.
`fos_elastica.index.app.user` and is an instance of `Elastica\Type`.
FOSElasticaBundle requires a provider for each type that will notify when an object
that maps to a type has been modified. The bundle ships with support for Doctrine and
@ -122,7 +135,7 @@ Below is an example for the Doctrine ORM.
There are a significant number of options available for types, that can be
[found here](types.md)
E) Populating the Elasticsearch index
E: Populating the Elasticsearch index
-------------------------------------
When using the providers and listeners that come with the bundle, any new or modified
@ -137,7 +150,7 @@ $ php app/console fos:elastica:populate
The command will also create all indexes and types defined if they do not already exist
on the Elasticsearch server.
F) Usage
F: Usage
--------
Usage documentation for the bundle is available [here](usage.md)

View file

@ -1,6 +1,34 @@
Type configuration
==================
Custom Property Paths
---------------------
Since FOSElasticaBundle 3.1.0, it is now possible to define custom property paths
to be used for data retrieval from the underlying model.
```yaml
user:
mappings:
username:
property_path: indexableUsername
firstName:
property_path: names[first]
```
This feature uses the Symfony PropertyAccessor component and supports all features
that the component supports.
The above example would retrieve an indexed field `username` from the property
`User->indexableUsername`, and the indexed field `firstName` would be populated from a
key `first` from an array on `User->names`.
Setting the property path to `false` will disable transformation of that value. In this
case the mapping will be created but no value will be populated while indexing. You can
populate this value by listening to the `POST_TRANSFORM` event emitted by this bundle.
See [cookbook/custom-properties.md](cookbook/custom-properties.md) for more information
about this event.
Handling missing results with FOSElasticaBundle
-----------------------------------------------

View file

@ -26,7 +26,9 @@ $userPaginator = $finder->findPaginated('bob');
$countOfResults = $userPaginator->getNbResults();
// Option 3b. KnpPaginator resultset
$paginator = $this->get('knp_paginator');
$results = $finder->createPaginatorAdapter('bob');
$pagination = $paginator->paginate($results, $page, 10);
```
Faceted Searching
@ -160,7 +162,7 @@ fos_elastica:
site:
settings:
index:
analysis:
analysis:
analyzer:
my_analyzer:
type: snowball

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -23,7 +23,7 @@
{% block menu %}
<span class="label">
<span class="icon"><img src="{{ asset('bundles/foselastica/images/elastica.png') }}" alt="" /></span>
<span class="icon"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAcCAQAAADPJofWAAAAAXNSR0IArs4c6QAAAAJiS0dEAP+Hj8y/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH2woEDg0xnxGaxwAAA51JREFUOMt100ts1FUUx/Hvufc/8x9mOrTTDpbysEVsqTzUqAsRgpqQGIJiIhtZSEJcqNGFJrSiZSPYkKALBUyAiLEuWKjBhdFAaEwwPNQqEAgEaA0FEWh4lE477/+9x0WLsST+Fmf1ycnJvecI/0knW4ENgZkhr5gVMtcEMmB64z1msCOCboR70tlkOuQdQREMBovF7tQt71/mXtzZQo8s89jLycFETiSqKd/v5wSY32VN18Ak3JGiR1d7an+Y3Z/Np7yVeCY/9czU4efF2iPxZ+wLXPgXP7WCTZ7MvvlnZlbqSJnGGbMeam5pk6HTIw9qs7tiH6D9vrbXW9e1Tus/sbhHZ8VOtfU1uZSExCUe1ExLtqayjcPnx4qNssjOs3TQrY9rYdVB2aFRw7HWq2kbJxCjFAsDoY+3hbkbw9ebNBsQ6EJVNlb3yQIDY8lcaAIsBisxiSXGToTNWqi/JUWXMIoabrK/95ymDNYHzmDUYAiwWOImOle+EI2AxyiKVydQGVAIoyneiBUNsAQEWIn+LA2NJqOYw3hUFQV2DFbu+FSuKTk9kRGMBoyPE7m8vza7UhMrmQNlV+NwAlDZXmXkkcHaeNamQBCEiLyeb7w631C71yxfr8/6YT8GUP6ifKlcf3xJX26kWNQyJfL+TvVs3S8rCw31hfRm2/KzL+iGQz8BnLjz8EVd5TPXF9wI/E0ZK/gr6SOLj7+Yz9RRu2bmMXm6y2/0A/rS0f6JL985+prVQAJiKEU8kLid6ezaA8Gh7iVP6BKtHad7njQr+xmSeFELLi2YovwlJ5Nbu07DNoLFoVNVrwBvh6feSMwSDcVuLvWF0y3Brd9mDlRlTsuVweGPCRzI+NOBW2rW3iYv5ZPfbrm7ji2fx1/Vkn7U/OGlivE4HA5Yh+yqUKFAYa3QfXcVf/R7Segy3wDGo4x3Tr3H3LLmqX6mZ5+jC4A5DO7jLR3SmAoYj8eTqx5uMR+UKErp78q2/e7ARNuLgE940bR1E9jpcKx+16MxJUd5+68XJp+a5DnqFxU/yYZG8XiSfJ+awkIW9WW+zLF8sh6NeqJb0cs+biIcKjW8u3T3xj+uZXZ/PbSe3km2nHWbogb3ZlSgPWz/bt71tsf432Qa64bSh5PTIFAU0pqdG3oRIiCgBBgsVTwRpYwmnWgMAlUd1aR+6m+DA4QqBlDKRDgifCqq8WO+CoFW9Ctd7dvBA+NV8DgiognuSv4bbsA/e9y1PQRZggAAAAAASUVORK5CYII=" alt="" /></span>
<strong>Elastica</strong>
<span class="count">
<span>{{ collector.querycount }}</span>

View file

@ -32,6 +32,10 @@ class PaginateElasticaQuerySubscriber implements EventSubscriberInterface
if (null != $facets) {
$event->setCustomPaginationParameter('facets', $facets);
}
$aggregations = $results->getAggregations();
if (null != $aggregations) {
$event->setCustomPaginationParameter('aggregations', $aggregations);
}
$event->stopPropagation();
}
@ -73,4 +77,4 @@ class PaginateElasticaQuerySubscriber implements EventSubscriberInterface
'knp_pager.items' => array('items', 1)
);
}
}
}

View file

@ -199,6 +199,24 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase
$this->assertCount(3, $configuration['indexes']['test']['types']['test']['properties']);
}
public function testUnconfiguredType()
{
$configuration = $this->getConfigs(array(
'clients' => array(
'default' => array('url' => 'http://localhost:9200'),
),
'indexes' => array(
'test' => array(
'types' => array(
'test' => null
)
)
)
));
$this->assertArrayHasKey('properties', $configuration['indexes']['test']['types']['test']);
}
public function testNestedProperties()
{
$this->getConfigs(array(

View file

@ -0,0 +1,32 @@
<?php
namespace FOS\ElasticaBundle\Tests\DependencyInjection;
use FOS\ElasticaBundle\DependencyInjection\FOSElasticaExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Yaml\Yaml;
class FOSElasticaExtensionTest extends \PHPUnit_Framework_TestCase
{
public function testShouldAddParentParamToObjectPersisterCall()
{
$config = Yaml::parse(file_get_contents(__DIR__.'/fixtures/config.yml'));
$containerBuilder = new ContainerBuilder;
$containerBuilder->setParameter('kernel.debug', true);
$extension = new FOSElasticaExtension;
$extension->load($config, $containerBuilder);
$this->assertTrue($containerBuilder->hasDefinition('fos_elastica.object_persister.test_index.child_field'));
$persisterCallDefinition = $containerBuilder->getDefinition('fos_elastica.object_persister.test_index.child_field');
$arguments = $persisterCallDefinition->getArguments();
$arguments = $arguments['index_3'];
$this->assertArrayHasKey('_parent', $arguments);
$this->assertEquals('parent_field', $arguments['_parent']['type']);
}
}

View file

@ -0,0 +1,21 @@
fos_elastica:
clients:
default:
url: http://localhost:9200
indexes:
test_index:
client: default
types:
parent_field:
mappings:
text: ~
persistence:
driver: orm
model: foo_model
child_field:
mappings:
text: ~
persistence:
driver: orm
model: foo_model
_parent: { type: "parent_field", property: "parent" }

View file

@ -16,7 +16,7 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase
$eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager());
$indexable = $this->getMockIndexable('index', 'type', $entity, true);
$listener = $this->createListener($persister, array(), $indexable, array('indexName' => 'index', 'typeName' => 'type'));
$listener = $this->createListener($persister, $indexable, array('indexName' => 'index', 'typeName' => 'type'));
$listener->postPersist($eventArgs);
$this->assertEquals($entity, current($listener->scheduledForInsertion));
@ -35,7 +35,7 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase
$eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager());
$indexable = $this->getMockIndexable('index', 'type', $entity, false);
$listener = $this->createListener($persister, array(), $indexable, array('indexName' => 'index', 'typeName' => 'type'));
$listener = $this->createListener($persister, $indexable, array('indexName' => 'index', 'typeName' => 'type'));
$listener->postPersist($eventArgs);
$this->assertEmpty($listener->scheduledForInsertion);
@ -55,7 +55,7 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase
$eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager());
$indexable = $this->getMockIndexable('index', 'type', $entity, true);
$listener = $this->createListener($persister, array(), $indexable, array('indexName' => 'index', 'typeName' => 'type'));
$listener = $this->createListener($persister, $indexable, array('indexName' => 'index', 'typeName' => 'type'));
$listener->postUpdate($eventArgs);
$this->assertEquals($entity, current($listener->scheduledForUpdate));
@ -89,7 +89,7 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase
->with($entity, 'id')
->will($this->returnValue($entity->getId()));
$listener = $this->createListener($persister, array(), $indexable, array('indexName' => 'index', 'typeName' => 'type'));
$listener = $this->createListener($persister, $indexable, array('indexName' => 'index', 'typeName' => 'type'));
$listener->postUpdate($eventArgs);
$this->assertEmpty($listener->scheduledForUpdate);
@ -124,7 +124,7 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase
->with($entity, 'id')
->will($this->returnValue($entity->getId()));
$listener = $this->createListener($persister, array(), $indexable, array('indexName' => 'index', 'typeName' => 'type'));
$listener = $this->createListener($persister, $indexable, array('indexName' => 'index', 'typeName' => 'type'));
$listener->preRemove($eventArgs);
$this->assertEquals($entity->getId(), current($listener->scheduledForDeletion));
@ -157,7 +157,7 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase
->with($entity, 'identifier')
->will($this->returnValue($entity->getId()));
$listener = $this->createListener($persister, array(), $indexable, array('identifier' => 'identifier', 'indexName' => 'index', 'typeName' => 'type'));
$listener = $this->createListener($persister, $indexable, array('identifier' => 'identifier', 'indexName' => 'index', 'typeName' => 'type'));
$listener->preRemove($eventArgs);
$this->assertEquals($entity->identifier, current($listener->scheduledForDeletion));

View file

@ -173,6 +173,32 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase
$provider->populate();
}
public function testPopulateShouldClearObjectManagerForFilteredBatch()
{
$nbObjects = 1;
$objects = array(1);
$provider = $this->getMockAbstractProvider();
$provider->expects($this->any())
->method('countObjects')
->will($this->returnValue($nbObjects));
$provider->expects($this->any())
->method('fetchSlice')
->will($this->returnValue($objects));
$this->indexable->expects($this->any())
->method('isObjectIndexable')
->with('index', 'type', $this->anything())
->will($this->returnValue(false));
$this->objectManager->expects($this->once())
->method('clear');
$provider->populate();
}
public function testPopulateInvokesLoggerClosure()
{
$nbObjects = 1;

View file

@ -0,0 +1,48 @@
<?php
/**
* This file is part of the FOSElasticaBundle project.
*
* (c) Tim Nagel <tim@nagel.com.au>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace FOS\ElasticaBundle\Tests\Functional;
use Symfony\Bundle\FrameworkBundle\Client;
/**
* @group functional
*/
class ClientTest extends WebTestCase
{
public function testContainerSource()
{
$client = $this->createClient(array('test_case' => 'Basic'));
$es = $client->getContainer()->get('fos_elastica.client.default');
$this->assertInstanceOf('Elastica\\Connection\\Strategy\\RoundRobin', $es->getConnectionStrategy());
$es = $client->getContainer()->get('fos_elastica.client.second_server');
$this->assertInstanceOf('Elastica\\Connection\\Strategy\\RoundRobin', $es->getConnectionStrategy());
$es = $client->getContainer()->get('fos_elastica.client.third');
$this->assertInstanceOf('Elastica\\Connection\\Strategy\\Simple', $es->getConnectionStrategy());
}
protected function setUp()
{
parent::setUp();
$this->deleteTmpDir('Basic');
}
protected function tearDown()
{
parent::tearDown();
$this->deleteTmpDir('Basic');
}
}

View file

@ -32,6 +32,10 @@ class MappingToElasticaTest extends WebTestCase
$this->assertTrue($mapping['type']['properties']['field1']['store']);
$this->assertArrayNotHasKey('store', $mapping['type']['properties']['field2']);
$type = $this->getType($client, 'type');
$mapping = $type->getMapping();
$this->assertEquals('parent', $mapping['type']['_parent']['type']);
$parent = $this->getType($client, 'parent');
$mapping = $parent->getMapping();
@ -49,6 +53,9 @@ class MappingToElasticaTest extends WebTestCase
$mapping = $type->getMapping();
$this->assertNotEmpty($mapping, 'Mapping was populated');
$this->assertFalse($mapping['type']['date_detection']);
$this->assertTrue($mapping['type']['numeric_detection']);
$this->assertEquals(array('yyyy-MM-dd'), $mapping['type']['dynamic_date_formats']);
$this->assertArrayHasKey('store', $mapping['type']['properties']['field1']);
$this->assertTrue($mapping['type']['properties']['field1']['store']);
$this->assertArrayNotHasKey('store', $mapping['type']['properties']['field2']);
@ -105,6 +112,7 @@ class MappingToElasticaTest extends WebTestCase
/**
* @param Client $client
* @param string $type
* @return \Elastica\Type
*/
private function getType(Client $client, $type = 'type')

View file

@ -0,0 +1,54 @@
<?php
/**
* This file is part of the FOSElasticaBundle project.
*
* (c) Tim Nagel <tim@nagel.com.au>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace FOS\ElasticaBundle\Tests\Functional;
use Elastica\Query\Match;
/**
* @group functional
*/
class PropertyPathTest extends WebTestCase
{
public function testContainerSource()
{
$client = $this->createClient(array('test_case' => 'ORM'));
/** @var \FOS\ElasticaBundle\Persister\ObjectPersister $persister */
$persister = $client->getContainer()->get('fos_elastica.object_persister.index.property_paths_type');
$obj = new TypeObj();
$obj->coll = 'Hello';
$persister->insertOne($obj);
/** @var \Elastica\Index $elClient */
$index = $client->getContainer()->get('fos_elastica.index.index');
$index->flush(true);
$query = new Match();
$query->setField('something', 'Hello');
$search = $index->createSearch($query);
$this->assertEquals(1, $search->count());
}
protected function setUp()
{
parent::setUp();
$this->deleteTmpDir('Basic');
}
protected function tearDown()
{
parent::tearDown();
$this->deleteTmpDir('Basic');
}
}

View file

@ -13,8 +13,10 @@ namespace FOS\ElasticaBundle\Tests\Functional;
class TypeObj
{
public $id = 5;
public $coll;
public $field1;
public $field2;
public function isIndexable()
{

View file

@ -15,7 +15,12 @@ fos_elastica:
- url: http://localhost:9200
- host: localhost
port: 9200
connectionStrategy: RoundRobin
second_server:
connections:
- url: http://localhost:9200
connection_strategy: RoundRobin
third:
url: http://localhost:9200
indexes:
index:
@ -46,6 +51,8 @@ fos_elastica:
index_analyzer: my_analyzer
type:
search_analyzer: my_analyzer
date_detection: false
dynamic_date_formats: [ 'yyyy-MM-dd' ]
dynamic_templates:
- dates:
match: "date_*"
@ -56,6 +63,7 @@ fos_elastica:
mapping:
analyzer: english
type: string
numeric_detection: true
properties:
field1: ~
field2:
@ -71,6 +79,11 @@ fos_elastica:
properties:
date: { boost: 5 }
content: ~
multiple:
type: "multi_field"
properties:
name: ~
position: ~
user:
type: "object"
approver:
@ -87,3 +100,4 @@ fos_elastica:
identifier: "id"
null_mappings:
mappings: ~
empty_index: ~

View file

@ -35,6 +35,8 @@ fos_elastica:
model: FOS\ElasticaBundle\Tests\Functional\TypeObj
listener:
is_indexable_callback: 'object.isIndexable() && !object.isntIndexable()'
provider:
debug_logging: true
type2:
properties:
field1: ~
@ -63,6 +65,18 @@ fos_elastica:
provider: ~
listener:
is_indexable_callback: [ 'FOS\ElasticaBundle\Tests\Functional\app\ORM\IndexableService', 'isntIndexable' ]
property_paths_type:
persistence:
driver: orm
model: FOS\ElasticaBundle\Tests\Functional\TypeObj
provider: ~
properties:
field1:
property_path: field2
something:
property_path: coll
dynamic:
property_path: false
second_index:
index_name: foselastica_orm_test_second_%kernel.environment%
types:

View file

@ -28,7 +28,7 @@ fos_elastica:
serializer: ~
indexes:
index:
index_name: foselastica_test_%kernel.environment%
index_name: foselastica_ser_test_%kernel.environment%
types:
type:
properties:

View file

@ -2,6 +2,7 @@
namespace FOS\ElasticaBundle\Tests\Transformer\ModelToElasticaAutoTransformer;
use FOS\ElasticaBundle\Event\TransformEvent;
use FOS\ElasticaBundle\Transformer\ModelToElasticaAutoTransformer;
use Symfony\Component\PropertyAccess\PropertyAccess;
@ -132,6 +133,35 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase
}
}
public function testTransformerDispatches()
{
$dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')
->getMock();
$dispatcher->expects($this->once())
->method('dispatch')
->with(
TransformEvent::POST_TRANSFORM,
$this->isInstanceOf('FOS\ElasticaBundle\Event\TransformEvent')
);
$transformer = $this->getTransformer($dispatcher);
$transformer->transform(new POPO(), array());
}
public function testPropertyPath()
{
$transformer = $this->getTransformer();
$document = $transformer->transform(new POPO(), array('name' => array('property_path' => false)));
$this->assertInstanceOf('Elastica\Document', $document);
$this->assertFalse($document->has('name'));
$document = $transformer->transform(new POPO(), array('realName' => array('property_path' => 'name')));
$this->assertInstanceOf('Elastica\Document', $document);
$this->assertTrue($document->has('realName'));
$this->assertEquals('someName', $document->get('realName'));
}
public function testThatCanTransformObject()
{
$transformer = $this->getTransformer();
@ -250,7 +280,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase
$document = $transformer->transform(new POPO(), array(
'sub' => array(
'type' => 'nested',
'properties' => array('foo' => '~')
'properties' => array('foo' => array())
)
));
$data = $document->getData();
@ -295,8 +325,8 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase
$this->assertTrue(array_key_exists('obj', $data));
$this->assertInternalType('array', $data['obj']);
$this->assertEquals(array(
'foo' => 'foo',
'bar' => 'foo',
'foo' => 'foo',
'bar' => 'foo',
'id' => 1
), $data['obj']);
}
@ -387,11 +417,12 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase
}
/**
* @param null|\Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* @return ModelToElasticaAutoTransformer
*/
private function getTransformer()
private function getTransformer($dispatcher = null)
{
$transformer = new ModelToElasticaAutoTransformer();
$transformer = new ModelToElasticaAutoTransformer(array(), $dispatcher);
$transformer->setPropertyAccessor(PropertyAccess::getPropertyAccessor());
return $transformer;

View file

@ -2,6 +2,8 @@
namespace FOS\ElasticaBundle\Transformer;
use FOS\ElasticaBundle\Event\TransformEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Elastica\Document;
@ -12,6 +14,11 @@ use Elastica\Document;
*/
class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterface
{
/**
* @var EventDispatcherInterface
*/
protected $dispatcher;
/**
* Optional parameters
*
@ -32,10 +39,12 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf
* Instanciates a new Mapper
*
* @param array $options
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(array $options = array())
public function __construct(array $options = array(), EventDispatcherInterface $dispatcher = null)
{
$this->options = array_merge($this->options, $options);
$this->dispatcher = $dispatcher;
}
/**
@ -66,16 +75,24 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf
$property = (null !== $mapping['property'])?$mapping['property']:$mapping['type'];
$value = $this->propertyAccessor->getValue($object, $property);
$document->setParent($this->propertyAccessor->getValue($value, $mapping['identifier']));
continue;
}
$value = $this->propertyAccessor->getValue($object, $key);
$path = isset($mapping['property_path']) ?
$mapping['property_path'] :
$key;
if (false === $path) {
continue;
}
$value = $this->propertyAccessor->getValue($object, $path);
if (isset($mapping['type']) && in_array($mapping['type'], array('nested', 'object')) && isset($mapping['properties']) && !empty($mapping['properties'])) {
/* $value is a nested document or object. Transform $value into
* an array of documents, respective the mapped properties.
*/
$document->set($key, $this->transformNested($value, $mapping['properties']));
continue;
}
@ -86,12 +103,20 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf
} else {
$document->addFileContent($key, $value);
}
continue;
}
$document->set($key, $this->normalizeValue($value));
}
if ($this->dispatcher) {
$event = new TransformEvent($document, $fields, $object);
$this->dispatcher->dispatch(TransformEvent::POST_TRANSFORM, $event);
$document = $event->getDocument();
}
return $document;
}

View file

@ -17,7 +17,7 @@
"symfony/console": "~2.1",
"symfony/form": "~2.1",
"symfony/property-access": "~2.2",
"ruflin/elastica": ">=0.90.10.0, <1.4-dev",
"ruflin/elastica": ">=0.90.10.0, <1.5-dev",
"psr/log": "~1.0"
},
"require-dev":{