diff --git a/.travis.yml b/.travis.yml index 02f3ab8..fbb22d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/CHANGELOG-3.0.md b/CHANGELOG-3.0.md index 2596a29..57ca45c 100644 --- a/CHANGELOG-3.0.md +++ b/CHANGELOG-3.0.md @@ -12,6 +12,29 @@ 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.9 (2015-03-12) + + * Fix a bug in the BC layer of the type configuration for empty configs + * Fix the service definition for the Doctrine listener when the logger is not enabled + +* 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 diff --git a/CHANGELOG-3.1.md b/CHANGELOG-3.1.md new file mode 100644 index 0000000..3ca3c77 --- /dev/null +++ b/CHANGELOG-3.1.md @@ -0,0 +1,61 @@ +CHANGELOG for 3.1.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 3.1 versions. + +To get the diff for a specific change, go to +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.3 (2015-04-02) + + * Fix Symfony 2.3 compatibility + +* 3.1.2 (2015-03-27) + + * Fix the previous release + +* 3.1.1 (2015-03-27) + + * Fix PopulateCommand trying to set formats for ProgressBar in Symfony < 2.5 + * Fix Provider implementations that depend on a batch size from going into + infinite loops + +* 3.1.0 (2015-03-18) + + * 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 + * Fixed a case where ProgressCommand would always ignore errors regardless of + --ignore-errors being passed or not. + * Added a `SliceFetcher` abstraction for Doctrine providers that get more + information about the previous slice allowing for optimising queries during + population. #725 + * New events `PRE_INDEX_POPULATE`, `POST_INDEX_POPULATE`, `PRE_TYPE_POPULATE` and + `POST_TYPE_POPULATE` allow for monitoring when an index is about to be or has + just been populated. #744 + * New events `PRE_INDEX_RESET`, `POST_INDEX_RESET`, `PRE_TYPE_RESET` and + `POST_TYPE_RESET` are run before and after operations that will reset an + index. #744 + * Added indexable callback support for the __invoke method of a service. #823 diff --git a/Command/PopulateCommand.php b/Command/PopulateCommand.php index f17ca4c..42af355 100644 --- a/Command/PopulateCommand.php +++ b/Command/PopulateCommand.php @@ -2,6 +2,8 @@ namespace FOS\ElasticaBundle\Command; +use FOS\ElasticaBundle\Event\IndexPopulateEvent; +use FOS\ElasticaBundle\Event\TypePopulateEvent; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Helper\DialogHelper; use Symfony\Component\Console\Input\InputOption; @@ -10,18 +12,28 @@ use Symfony\Component\Console\Output\OutputInterface; 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 + * Populate the search index. */ class PopulateCommand extends ContainerAwareCommand { + /** + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + private $dispatcher; + /** * @var IndexManager */ private $indexManager; + /** + * @var ProgressClosureBuilder + */ + private $progressClosureBuilder; + /** * @var ProviderRegistry */ @@ -46,31 +58,46 @@ class PopulateCommand extends ContainerAwareCommand ->addOption('sleep', null, InputOption::VALUE_REQUIRED, 'Sleep time between persisting iterations (microseconds)', 0) ->addOption('batch-size', null, InputOption::VALUE_REQUIRED, 'Index packet size (overrides provider config option)') ->addOption('ignore-errors', null, InputOption::VALUE_NONE, 'Do not stop on errors') + ->addOption('no-overwrite-format', null, InputOption::VALUE_NONE, 'Prevent this command from overwriting ProgressBar\'s formats') ->setDescription('Populates search indexes from providers') ; } /** - * @see Symfony\Component\Console\Command\Command::initialize() + * {@inheritDoc} */ protected function initialize(InputInterface $input, OutputInterface $output) { + $this->dispatcher = $this->getContainer()->get('event_dispatcher'); $this->indexManager = $this->getContainer()->get('fos_elastica.index_manager'); $this->providerRegistry = $this->getContainer()->get('fos_elastica.provider_registry'); $this->resetter = $this->getContainer()->get('fos_elastica.resetter'); + $this->progressClosureBuilder = new ProgressClosureBuilder(); + + if (!$input->getOption('no-overwrite-format') && class_exists('Symfony\\Component\\Console\\Helper\\ProgressBar')) { + 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%"); + } } /** - * @see Symfony\Component\Console\Command\Command::execute() + * {@inheritDoc} */ protected function execute(InputInterface $input, OutputInterface $output) { - $index = $input->getOption('index'); - $type = $input->getOption('type'); - $reset = !$input->getOption('no-reset'); - $options = $input->getOptions(); - - $options['ignore-errors'] = $input->hasOption('ignore-errors'); + $index = $input->getOption('index'); + $type = $input->getOption('type'); + $reset = !$input->getOption('no-reset'); + $options = array( + 'ignore_errors' => $input->getOption('ignore-errors'), + 'offset' => $input->getOption('offset'), + 'sleep' => $input->getOption('sleep') + ); + if ($input->getOption('batch-size')) { + $options['batch_size'] = (int) $input->getOption('batch-size'); + } if ($input->isInteractive() && $reset && $input->getOption('offset')) { /** @var DialogHelper $dialog */ @@ -109,25 +136,22 @@ class PopulateCommand extends ContainerAwareCommand */ private function populateIndex(OutputInterface $output, $index, $reset, $options) { - if ($reset) { + $event = new IndexPopulateEvent($index, $reset, $options); + $this->dispatcher->dispatch(IndexPopulateEvent::PRE_INDEX_POPULATE, $event); + + if ($event->isReset()) { $output->writeln(sprintf('Resetting %s', $index)); $this->resetter->resetIndex($index, true); } - /** @var $providers ProviderInterface[] */ - $providers = $this->providerRegistry->getIndexProviders($index); - - foreach ($providers as $type => $provider) { - $loggerClosure = function($message) use ($output, $index, $type) { - $output->writeln(sprintf('Populating %s/%s, %s', $index, $type, $message)); - }; - - $provider->populate($loggerClosure, $options); + $types = array_keys($this->providerRegistry->getIndexProviders($index)); + foreach ($types as $type) { + $this->populateIndexType($output, $index, $type, false, $event->getOptions()); } - $output->writeln(sprintf('Refreshing %s', $index)); - $this->resetter->postPopulate($index); - $this->indexManager->getIndex($index)->refresh(); + $this->dispatcher->dispatch(IndexPopulateEvent::POST_INDEX_POPULATE, $event); + + $this->refreshIndex($output, $index); } /** @@ -141,17 +165,35 @@ class PopulateCommand extends ContainerAwareCommand */ private function populateIndexType(OutputInterface $output, $index, $type, $reset, $options) { - if ($reset) { + $event = new TypePopulateEvent($index, $type, $reset, $options); + $this->dispatcher->dispatch(TypePopulateEvent::PRE_TYPE_POPULATE, $event); + + if ($event->isReset()) { $output->writeln(sprintf('Resetting %s/%s', $index, $type)); $this->resetter->resetIndexType($index, $type); } - $loggerClosure = function($message) use ($output, $index, $type) { - $output->writeln(sprintf('Populating %s/%s, %s', $index, $type, $message)); - }; - $provider = $this->providerRegistry->getProvider($index, $type); - $provider->populate($loggerClosure, $options); + $loggerClosure = $this->progressClosureBuilder->build($output, 'Populating', $index, $type); + $provider->populate($loggerClosure, $event->getOptions()); + + $this->dispatcher->dispatch(TypePopulateEvent::POST_TYPE_POPULATE, $event); + + $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('Refreshing %s', $index)); $this->indexManager->getIndex($index)->refresh(); diff --git a/Command/ProgressClosureBuilder.php b/Command/ProgressClosureBuilder.php new file mode 100644 index 0000000..f244bc3 --- /dev/null +++ b/Command/ProgressClosureBuilder.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Command; + +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\OutputInterface; + +class ProgressClosureBuilder +{ + /** + * Builds a loggerClosure to be called from inside the Provider to update the command + * line. + * + * @param OutputInterface $output + * @param string $action + * @param string $index + * @param string $type + * + * @return callable + */ + public function build(OutputInterface $output, $action, $index, $type) + { + if (!class_exists('Symfony\Component\Console\Helper\ProgressBar') || + !is_callable(array('Symfony\Component\Console\Helper\ProgressBar', 'getProgress'))) { + return $this->buildLegacy($output, $action, $index, $type); + } + + $progress = null; + + return function ($increment, $totalObjects, $message = null) use (&$progress, $output, $action, $index, $type) { + if (null === $progress) { + $progress = new ProgressBar($output, $totalObjects); + $progress->start(); + } + + if (null !== $message) { + $progress->clear(); + $output->writeln(sprintf('%s %s', $action, $message)); + $progress->display(); + } + + $progress->setMessage(sprintf('%s %s/%s', $action, $index, $type)); + $progress->advance($increment); + }; + } + + /** + * Builds a legacy closure that outputs lines for each step. Used in cases + * where the ProgressBar component doesnt exist or does not have the correct + * methods to support what we need. + * + * @param OutputInterface $output + * @param string $action + * @param string $index + * @param string $type + * + * @return callable + */ + private function buildLegacy(OutputInterface $output, $action, $index, $type) + { + $lastStep = null; + $current = 0; + + return function ($increment, $totalObjects, $message = null) use ($output, $action, $index, $type, &$lastStep, &$current) { + if ($current + $increment > $totalObjects) { + $increment = $totalObjects - $current; + } + + if (null !== $message) { + $output->writeln(sprintf('%s %s', $action, $message)); + } + + $currentTime = microtime(true); + $timeDifference = $currentTime - $lastStep; + $objectsPerSecond = $lastStep ? ($increment / $timeDifference) : $increment; + $lastStep = $currentTime; + $current += $increment; + $percent = 100 * $current / $totalObjects; + + $output->writeln(sprintf( + '%s %s/%s %0.1f%% (%d/%d), %d objects/s (RAM: current=%uMo peak=%uMo)', + $action, + $index, + $type, + $percent, + $current, + $totalObjects, + $objectsPerSecond, + round(memory_get_usage() / (1024 * 1024)), + round(memory_get_peak_usage() / (1024 * 1024)) + )); + }; + } +} diff --git a/Command/ResetCommand.php b/Command/ResetCommand.php index 06cfe48..85c5483 100755 --- a/Command/ResetCommand.php +++ b/Command/ResetCommand.php @@ -10,7 +10,7 @@ use FOS\ElasticaBundle\IndexManager; use FOS\ElasticaBundle\Resetter; /** - * Reset search indexes + * Reset search indexes. */ class ResetCommand extends ContainerAwareCommand { @@ -33,6 +33,7 @@ class ResetCommand extends ContainerAwareCommand ->setName('fos:elastica:reset') ->addOption('index', null, InputOption::VALUE_OPTIONAL, 'The index to reset') ->addOption('type', null, InputOption::VALUE_OPTIONAL, 'The type to reset') + ->addOption('force', null, InputOption::VALUE_NONE, 'Force index deletion if same name as alias') ->setDescription('Reset search indexes') ; } @@ -51,8 +52,9 @@ class ResetCommand extends ContainerAwareCommand */ protected function execute(InputInterface $input, OutputInterface $output) { - $index = $input->getOption('index'); - $type = $input->getOption('type'); + $index = $input->getOption('index'); + $type = $input->getOption('type'); + $force = (bool) $input->getOption('force'); if (null === $index && null !== $type) { throw new \InvalidArgumentException('Cannot specify type option without an index.'); @@ -69,7 +71,7 @@ class ResetCommand extends ContainerAwareCommand foreach ($indexes as $index) { $output->writeln(sprintf('Resetting %s', $index)); - $this->resetter->resetIndex($index); + $this->resetter->resetIndex($index, false, $force); } } } diff --git a/Command/SearchCommand.php b/Command/SearchCommand.php index ec7cfb7..11183de 100644 --- a/Command/SearchCommand.php +++ b/Command/SearchCommand.php @@ -11,7 +11,7 @@ use Elastica\Query; use Elastica\Result; /** - * Searches a type + * Searches a type. */ class SearchCommand extends ContainerAwareCommand { diff --git a/Configuration/IndexConfig.php b/Configuration/IndexConfig.php index 7416424..749b10d 100644 --- a/Configuration/IndexConfig.php +++ b/Configuration/IndexConfig.php @@ -53,9 +53,9 @@ class IndexConfig /** * Constructor expects an array as generated by the Container Configuration builder. * - * @param string $name + * @param string $name * @param TypeConfig[] $types - * @param array $config + * @param array $config */ public function __construct($name, array $types, array $config) { @@ -92,7 +92,9 @@ class IndexConfig /** * @param string $typeName + * * @return TypeConfig + * * @throws \InvalidArgumentException */ public function getType($typeName) diff --git a/Configuration/ManagerInterface.php b/Configuration/ManagerInterface.php index 96d510f..742df1b 100644 --- a/Configuration/ManagerInterface.php +++ b/Configuration/ManagerInterface.php @@ -20,6 +20,7 @@ interface ManagerInterface * Returns configuration for an index. * * @param $index + * * @return IndexConfig */ public function getIndexConfiguration($index); @@ -36,6 +37,7 @@ interface ManagerInterface * * @param string $index * @param string $type + * * @return TypeConfig */ public function getTypeConfiguration($index, $type); diff --git a/Configuration/Search.php b/Configuration/Search.php index 1306f92..1d046c0 100644 --- a/Configuration/Search.php +++ b/Configuration/Search.php @@ -17,9 +17,10 @@ use FOS\ElasticaBundle\Annotation\Search as BaseSearch; * Annotation class for setting search repository. * * @Annotation + * * @deprecated Use FOS\ElasticaBundle\Annotation\Search instead * @Target("CLASS") */ class Search extends BaseSearch { -} +} diff --git a/Configuration/Source/ContainerSource.php b/Configuration/Source/ContainerSource.php index 8d094c7..25e6f86 100644 --- a/Configuration/Source/ContainerSource.php +++ b/Configuration/Source/ContainerSource.php @@ -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,29 @@ 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; + } } diff --git a/Configuration/Source/SourceInterface.php b/Configuration/Source/SourceInterface.php index 34e9901..05a64d0 100644 --- a/Configuration/Source/SourceInterface.php +++ b/Configuration/Source/SourceInterface.php @@ -23,4 +23,4 @@ interface SourceInterface * @return \FOS\ElasticaBundle\Configuration\IndexConfig[] */ public function getConfiguration(); -} +} diff --git a/Configuration/TypeConfig.php b/Configuration/TypeConfig.php index 3f1c939..a46cd34 100644 --- a/Configuration/TypeConfig.php +++ b/Configuration/TypeConfig.php @@ -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 */ @@ -77,6 +101,9 @@ class TypeConfig return $this->getConfig('search_analyzer'); } + /** + * @param string $key + */ private function getConfig($key) { return isset($this->config[$key]) ? diff --git a/DependencyInjection/Compiler/ConfigSourcePass.php b/DependencyInjection/Compiler/ConfigSourcePass.php index b35a665..92a2489 100644 --- a/DependencyInjection/Compiler/ConfigSourcePass.php +++ b/DependencyInjection/Compiler/ConfigSourcePass.php @@ -33,4 +33,4 @@ class ConfigSourcePass implements CompilerPassInterface $container->getDefinition('fos_elastica.config_manager')->replaceArgument(0, $sources); } -} +} diff --git a/DependencyInjection/Compiler/RegisterProvidersPass.php b/DependencyInjection/Compiler/RegisterProvidersPass.php index c6c9e6e..4fd25b0 100644 --- a/DependencyInjection/Compiler/RegisterProvidersPass.php +++ b/DependencyInjection/Compiler/RegisterProvidersPass.php @@ -55,6 +55,7 @@ class RegisterProvidersPass implements CompilerPassInterface * Returns whether the class implements ProviderInterface. * * @param string $class + * * @return boolean */ private function isProviderImplementation($class) diff --git a/DependencyInjection/Compiler/TransformerPass.php b/DependencyInjection/Compiler/TransformerPass.php index 4281d0b..596c732 100644 --- a/DependencyInjection/Compiler/TransformerPass.php +++ b/DependencyInjection/Compiler/TransformerPass.php @@ -31,7 +31,7 @@ class TransformerPass implements CompilerPassInterface throw new InvalidArgumentException('The Transformer must have both a type and an index defined.'); } - $transformers[$tag['index']][$tag['type']]= new Reference($id); + $transformers[$tag['index']][$tag['type']] = new Reference($id); } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 552f61b..1391eaa 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -30,7 +30,7 @@ class Configuration implements ConfigurationInterface /** * Generates the configuration tree. * - * @return \Symfony\Component\Config\Definition\NodeInterface + * @return TreeBuilder */ public function getConfigTreeBuilder() { @@ -63,7 +63,7 @@ class Configuration implements ConfigurationInterface } /** - * Adds the configuration for the "clients" key + * Adds the configuration for the "clients" key. */ private function addClientsSection(ArrayNodeDefinition $rootNode) { @@ -76,20 +76,30 @@ class Configuration implements ConfigurationInterface ->performNoDeepMerging() // BC - Renaming 'servers' node to 'connections' ->beforeNormalization() - ->ifTrue(function($v) { return isset($v['servers']); }) - ->then(function($v) { + ->ifTrue(function ($v) { return isset($v['servers']); }) + ->then(function ($v) { $v['connections'] = $v['servers']; unset($v['servers']); 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); }) ->then(function ($v) { return array( - 'connections' => array($v) + 'connections' => array($v), ); }) ->end() @@ -101,8 +111,8 @@ class Configuration implements ConfigurationInterface ->children() ->scalarNode('url') ->validate() - ->ifTrue(function($url) { return $url && substr($url, -1) !== '/'; }) - ->then(function($url) { return $url.'/'; }) + ->ifTrue(function ($url) { return $url && substr($url, -1) !== '/'; }) + ->then(function ($url) { return $url.'/'; }) ->end() ->end() ->scalarNode('host')->end() @@ -124,6 +134,7 @@ class Configuration implements ConfigurationInterface ->end() ->scalarNode('timeout')->end() ->scalarNode('headers')->end() + ->scalarNode('connectionStrategy')->defaultValue('Simple')->end() ->end() ->end() ->end() @@ -132,7 +143,7 @@ class Configuration implements ConfigurationInterface } /** - * Adds the configuration for the "indexes" key + * Adds the configuration for the "indexes" key. */ private function addIndexesSection(ArrayNodeDefinition $rootNode) { @@ -181,10 +192,14 @@ 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); }) - ->then(function($v) { + ->ifTrue(function ($v) { return array_key_exists('mappings', $v); }) + ->then(function ($v) { $v['properties'] = $v['mappings']; unset($v['mappings']); @@ -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()) @@ -394,7 +422,7 @@ class Configuration implements ConfigurationInterface } /** - * Returns the array node used for "_all" + * Returns the array node used for "_all". */ protected function getAllNode() { @@ -413,7 +441,7 @@ class Configuration implements ConfigurationInterface } /** - * Returns the array node used for "_timestamp" + * Returns the array node used for "_timestamp". */ protected function getTimestampNode() { @@ -434,7 +462,7 @@ class Configuration implements ConfigurationInterface } /** - * Returns the array node used for "_ttl" + * Returns the array node used for "_ttl". */ protected function getTtlNode() { @@ -463,9 +491,9 @@ class Configuration implements ConfigurationInterface $node ->validate() - ->ifTrue(function($v) { return isset($v['driver']) && 'propel' === $v['driver'] && isset($v['listener']); }) + ->ifTrue(function ($v) { return isset($v['driver']) && 'propel' === $v['driver'] && isset($v['listener']); }) ->thenInvalid('Propel doesn\'t support listeners') - ->ifTrue(function($v) { return isset($v['driver']) && 'propel' === $v['driver'] && isset($v['repository']); }) + ->ifTrue(function ($v) { return isset($v['driver']) && 'propel' === $v['driver'] && isset($v['repository']); }) ->thenInvalid('Propel doesn\'t support the "repository" parameter') ->end() ->children() @@ -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() diff --git a/DependencyInjection/FOSElasticaExtension.php b/DependencyInjection/FOSElasticaExtension.php index cdf109b..ec45e25 100644 --- a/DependencyInjection/FOSElasticaExtension.php +++ b/DependencyInjection/FOSElasticaExtension.php @@ -80,9 +80,10 @@ class FOSElasticaExtension extends Extension } /** - * @param array $config + * @param array $config * @param ContainerBuilder $container - * @return Configuration|null|\Symfony\Component\Config\Definition\ConfigurationInterface + * + * @return Configuration */ public function getConfiguration(array $config, ContainerBuilder $container) { @@ -92,8 +93,9 @@ class FOSElasticaExtension extends Extension /** * Loads the configured clients. * - * @param array $clients An array of clients configurations + * @param array $clients An array of clients configurations * @param ContainerBuilder $container A ContainerBuilder instance + * * @return array */ private function loadClients(array $clients, ContainerBuilder $container) @@ -114,7 +116,7 @@ class FOSElasticaExtension extends Extension $this->clients[$name] = array( 'id' => $clientId, - 'reference' => new Reference($clientId) + 'reference' => new Reference($clientId), ); } } @@ -122,9 +124,11 @@ class FOSElasticaExtension extends Extension /** * Loads the configured indexes. * - * @param array $indexes An array of indexes configurations + * @param array $indexes An array of indexes configurations * @param ContainerBuilder $container A ContainerBuilder instance + * * @throws \InvalidArgumentException + * * @return array */ private function loadIndexes(array $indexes, ContainerBuilder $container) @@ -133,7 +137,7 @@ class FOSElasticaExtension extends Extension foreach ($indexes as $name => $index) { $indexId = sprintf('fos_elastica.index.%s', $name); - $indexName = isset($index['index_name']) ? $index['index_name']: $name; + $indexName = isset($index['index_name']) ? $index['index_name'] : $name; $indexDef = new DefinitionDecorator('fos_elastica.index_prototype'); $indexDef->replaceArgument(0, $indexName); @@ -173,8 +177,9 @@ class FOSElasticaExtension extends Extension * Loads the configured index finders. * * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container - * @param string $name The index name - * @param Reference $index Reference to the related index + * @param string $name The index name + * @param Reference $index Reference to the related index + * * @return string */ private function loadIndexFinder(ContainerBuilder $container, $name, Reference $index) @@ -197,10 +202,10 @@ class FOSElasticaExtension extends Extension /** * Loads the configured types. * - * @param array $types + * @param array $types * @param ContainerBuilder $container - * @param array $indexConfig - * @param array $indexableCallbacks + * @param array $indexConfig + * @param array $indexableCallbacks */ private function loadTypes(array $types, ContainerBuilder $container, array $indexConfig, array &$indexableCallbacks) { @@ -241,6 +246,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] : @@ -277,13 +285,13 @@ class FOSElasticaExtension extends Extension } /** - * Loads the optional provider and finder for a type + * Loads the optional provider and finder for a type. * - * @param array $typeConfig + * @param array $typeConfig * @param ContainerBuilder $container - * @param Reference $typeRef - * @param string $indexName - * @param string $typeName + * @param Reference $typeRef + * @param string $indexName + * @param string $typeName */ private function loadTypePersistenceIntegration(array $typeConfig, ContainerBuilder $container, Reference $typeRef, $indexName, $typeName) { @@ -307,10 +315,11 @@ class FOSElasticaExtension extends Extension /** * Creates and loads an ElasticaToModelTransformer. * - * @param array $typeConfig + * @param array $typeConfig * @param ContainerBuilder $container - * @param string $indexName - * @param string $typeName + * @param string $indexName + * @param string $typeName + * * @return string */ private function loadElasticaToModelTransformer(array $typeConfig, ContainerBuilder $container, $indexName, $typeName) @@ -342,10 +351,11 @@ class FOSElasticaExtension extends Extension /** * Creates and loads a ModelToElasticaTransformer for an index/type. * - * @param array $typeConfig + * @param array $typeConfig * @param ContainerBuilder $container - * @param string $indexName - * @param string $typeName + * @param string $indexName + * @param string $typeName + * * @return string */ private function loadModelToElasticaTransformer(array $typeConfig, ContainerBuilder $container, $indexName, $typeName) @@ -361,7 +371,7 @@ class FOSElasticaExtension extends Extension $serviceId = sprintf('fos_elastica.model_to_elastica_transformer.%s.%s', $indexName, $typeName); $serviceDef = new DefinitionDecorator($abstractId); $serviceDef->replaceArgument(0, array( - 'identifier' => $typeConfig['identifier'] + 'identifier' => $typeConfig['identifier'], )); $container->setDefinition($serviceId, $serviceDef); @@ -371,12 +381,13 @@ class FOSElasticaExtension extends Extension /** * Creates and loads an object persister for a type. * - * @param array $typeConfig - * @param Reference $typeRef + * @param array $typeConfig + * @param Reference $typeRef * @param ContainerBuilder $container - * @param string $indexName - * @param string $typeName - * @param string $transformerId + * @param string $indexName + * @param string $typeName + * @param string $transformerId + * * @return string */ private function loadObjectPersister(array $typeConfig, Reference $typeRef, ContainerBuilder $container, $indexName, $typeName, $transformerId) @@ -393,7 +404,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); @@ -410,11 +426,12 @@ class FOSElasticaExtension extends Extension /** * Loads a provider for a type. * - * @param array $typeConfig + * @param array $typeConfig * @param ContainerBuilder $container - * @param string $objectPersisterId - * @param string $indexName - * @param string $typeName + * @param string $objectPersisterId + * @param string $indexName + * @param string $typeName + * * @return string */ private function loadTypeProvider(array $typeConfig, ContainerBuilder $container, $objectPersisterId, $indexName, $typeName) @@ -427,7 +444,7 @@ class FOSElasticaExtension extends Extension * index and type names were "prototype" and a driver, respectively. */ $providerId = sprintf('fos_elastica.provider.%s.%s', $indexName, $typeName); - $providerDef = new DefinitionDecorator('fos_elastica.provider.prototype.' . $typeConfig['driver']); + $providerDef = new DefinitionDecorator('fos_elastica.provider.prototype.'.$typeConfig['driver']); $providerDef->addTag('fos_elastica.provider', array('index' => $indexName, 'type' => $typeName)); $providerDef->replaceArgument(0, new Reference($objectPersisterId)); $providerDef->replaceArgument(2, $typeConfig['model']); @@ -444,11 +461,12 @@ class FOSElasticaExtension extends Extension /** * Loads doctrine listeners to handle indexing of new or updated objects. * - * @param array $typeConfig + * @param array $typeConfig * @param ContainerBuilder $container - * @param string $objectPersisterId - * @param string $indexName - * @param string $typeName + * @param string $objectPersisterId + * @param string $indexName + * @param string $typeName + * * @return string */ private function loadTypeListener(array $typeConfig, ContainerBuilder $container, $objectPersisterId, $indexName, $typeName) @@ -464,19 +482,30 @@ 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, $typeConfig['listener']['logger'] ? + new Reference($typeConfig['listener']['logger']) : + null + ); + + $tagName = null; + switch ($typeConfig['driver']) { + case 'orm': + $tagName = 'doctrine.event_listener'; + break; + case 'mongodb': + $tagName = 'doctrine_mongodb.odm.event_listener'; + break; } - switch ($typeConfig['driver']) { - case 'orm': $listenerDef->addTag('doctrine.event_subscriber'); break; - case 'mongodb': $listenerDef->addTag('doctrine_mongodb.odm.event_subscriber'); break; + if (null !== $tagName) { + foreach ($this->getDoctrineEvents($typeConfig) as $event) { + $listenerDef->addTag($tagName, array('event' => $event)); + } } $container->setDefinition($listenerId, $listenerDef); @@ -485,7 +514,7 @@ class FOSElasticaExtension extends Extension } /** - * Map Elastica to Doctrine events for the current driver + * Map Elastica to Doctrine events for the current driver. */ private function getDoctrineEvents(array $typeConfig) { @@ -498,7 +527,6 @@ class FOSElasticaExtension extends Extension break; default: throw new InvalidArgumentException(sprintf('Cannot determine events for driver "%s"', $typeConfig['driver'])); - break; } $events = array(); @@ -506,7 +534,7 @@ class FOSElasticaExtension extends Extension 'insert' => array(constant($eventsClass.'::postPersist')), 'update' => array(constant($eventsClass.'::postUpdate')), 'delete' => array(constant($eventsClass.'::preRemove')), - 'flush' => array($typeConfig['listener']['immediate'] ? constant($eventsClass.'::preFlush') : constant($eventsClass.'::postFlush')) + 'flush' => array($typeConfig['listener']['immediate'] ? constant($eventsClass.'::preFlush') : constant($eventsClass.'::postFlush')), ); foreach ($eventMapping as $event => $doctrineEvents) { @@ -521,12 +549,13 @@ class FOSElasticaExtension extends Extension /** * Loads a Type specific Finder. * - * @param array $typeConfig + * @param array $typeConfig * @param ContainerBuilder $container - * @param $elasticaToModelId - * @param Reference $typeRef - * @param string $indexName - * @param string $typeName + * @param string $elasticaToModelId + * @param Reference $typeRef + * @param string $indexName + * @param string $typeName + * * @return string */ private function loadTypeFinder(array $typeConfig, ContainerBuilder $container, $elasticaToModelId, Reference $typeRef, $indexName, $typeName) @@ -553,7 +582,7 @@ class FOSElasticaExtension extends Extension } /** - * Loads the index manager + * Loads the index manager. * * @param ContainerBuilder $container **/ @@ -571,7 +600,7 @@ class FOSElasticaExtension extends Extension * Makes sure a specific driver has been loaded. * * @param ContainerBuilder $container - * @param string $driver + * @param string $driver */ private function loadDriver(ContainerBuilder $container, $driver) { @@ -587,7 +616,7 @@ class FOSElasticaExtension extends Extension /** * Loads and configures the serializer prototype. * - * @param array $config + * @param array $config * @param ContainerBuilder $container */ private function loadSerializer($config, ContainerBuilder $container) @@ -606,7 +635,7 @@ class FOSElasticaExtension extends Extension /** * Creates a default manager alias for defined default manager or the first loaded driver. * - * @param string $defaultManager + * @param string $defaultManager * @param ContainerBuilder $container */ private function createDefaultManagerAlias($defaultManager, ContainerBuilder $container) @@ -630,7 +659,9 @@ class FOSElasticaExtension extends Extension * Returns a reference to a client given its configured name. * * @param string $clientName + * * @return Reference + * * @throws \InvalidArgumentException */ private function getClient($clientName) diff --git a/Doctrine/AbstractElasticaToModelTransformer.php b/Doctrine/AbstractElasticaToModelTransformer.php index 96f73bd..0263f42 100755 --- a/Doctrine/AbstractElasticaToModelTransformer.php +++ b/Doctrine/AbstractElasticaToModelTransformer.php @@ -2,32 +2,34 @@ namespace FOS\ElasticaBundle\Doctrine; +use Doctrine\Common\Persistence\ManagerRegistry; use FOS\ElasticaBundle\HybridResult; -use FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerInterface; +use FOS\ElasticaBundle\Transformer\AbstractElasticaToModelTransformer as BaseTransformer; use FOS\ElasticaBundle\Transformer\HighlightableModelInterface; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * Maps Elastica documents with Doctrine objects * This mapper assumes an exact match between - * elastica documents ids and doctrine object ids + * elastica documents ids and doctrine object ids. */ -abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTransformerInterface +abstract class AbstractElasticaToModelTransformer extends BaseTransformer { /** - * Manager registry + * Manager registry. + * + * @var ManagerRegistry */ protected $registry = null; /** - * Class of the model to map to the elastica documents + * Class of the model to map to the elastica documents. * * @var string */ protected $objectClass = null; /** - * Optional parameters + * Optional parameters. * * @var array */ @@ -39,20 +41,13 @@ abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTran ); /** - * PropertyAccessor instance + * Instantiates a new Mapper. * - * @var PropertyAccessorInterface - */ - protected $propertyAccessor; - - /** - * Instantiates a new Mapper - * - * @param object $registry + * @param ManagerRegistry $registry * @param string $objectClass - * @param array $options + * @param array $options */ - public function __construct($registry, $objectClass, array $options = array()) + public function __construct(ManagerRegistry $registry, $objectClass, array $options = array()) { $this->registry = $registry; $this->objectClass = $objectClass; @@ -69,22 +64,14 @@ abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTran return $this->objectClass; } - /** - * Set the PropertyAccessor - * - * @param PropertyAccessorInterface $propertyAccessor - */ - public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor) - { - $this->propertyAccessor = $propertyAccessor; - } - /** * Transforms an array of elastica objects into an array of - * model objects fetched from the doctrine repository + * model objects fetched from the doctrine repository. * * @param array $elasticaObjects of elastica objects + * * @throws \RuntimeException + * * @return array **/ public function transform(array $elasticaObjects) @@ -109,11 +96,7 @@ abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTran // sort objects in the order of ids $idPos = array_flip($ids); $identifier = $this->options['identifier']; - $propertyAccessor = $this->propertyAccessor; - usort($objects, function($a, $b) use ($idPos, $identifier, $propertyAccessor) - { - return $idPos[$propertyAccessor->getValue($a, $identifier)] > $idPos[$propertyAccessor->getValue($b, $identifier)]; - }); + usort($objects, $this->getSortingClosure($idPos, $identifier)); return $objects; } @@ -137,7 +120,7 @@ abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTran } /** - * {@inheritdoc} + * {@inheritDoc} */ public function getIdentifierField() { @@ -145,11 +128,12 @@ abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTran } /** - * Fetches objects by theses identifier values + * Fetches objects by theses identifier values. + * + * @param array $identifierValues ids values + * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays * - * @param array $identifierValues ids values - * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays * @return array of objects or arrays */ - protected abstract function findByIdentifiers(array $identifierValues, $hydrate); + abstract protected function findByIdentifiers(array $identifierValues, $hydrate); } diff --git a/Doctrine/AbstractProvider.php b/Doctrine/AbstractProvider.php index a662fd4..ec198a8 100644 --- a/Doctrine/AbstractProvider.php +++ b/Doctrine/AbstractProvider.php @@ -7,125 +7,61 @@ use Elastica\Exception\Bulk\ResponseException as BulkResponseException; use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; use FOS\ElasticaBundle\Provider\AbstractProvider as BaseAbstractProvider; use FOS\ElasticaBundle\Provider\IndexableInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; abstract class AbstractProvider extends BaseAbstractProvider { + /** + * @var SliceFetcherInterface + */ + private $sliceFetcher; + + /** + * @var ManagerRegistry + */ protected $managerRegistry; /** * Constructor. * * @param ObjectPersisterInterface $objectPersister - * @param IndexableInterface $indexable - * @param string $objectClass - * @param array $options - * @param ManagerRegistry $managerRegistry + * @param IndexableInterface $indexable + * @param string $objectClass + * @param array $baseOptions + * @param ManagerRegistry $managerRegistry + * @param SliceFetcherInterface $sliceFetcher */ public function __construct( ObjectPersisterInterface $objectPersister, IndexableInterface $indexable, $objectClass, - array $options, - ManagerRegistry $managerRegistry + array $baseOptions, + ManagerRegistry $managerRegistry, + SliceFetcherInterface $sliceFetcher = null ) { - parent::__construct($objectPersister, $indexable, $objectClass, array_merge(array( - 'clear_object_manager' => true, - 'debug_logging' => false, - 'ignore_errors' => false, - 'query_builder_method' => 'createQueryBuilder', - ), $options)); + parent::__construct($objectPersister, $indexable, $objectClass, $baseOptions); $this->managerRegistry = $managerRegistry; - } - - /** - * @see FOS\ElasticaBundle\Provider\ProviderInterface::populate() - */ - public function populate(\Closure $loggerClosure = null, array $options = array()) - { - if (!$this->options['debug_logging']) { - $logger = $this->disableLogging(); - } - - $queryBuilder = $this->createQueryBuilder(); - $nbObjects = $this->countObjects($queryBuilder); - $offset = isset($options['offset']) ? intval($options['offset']) : 0; - $sleep = isset($options['sleep']) ? intval($options['sleep']) : 0; - $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); - - for (; $offset < $nbObjects; $offset += $batchSize) { - if ($loggerClosure) { - $stepStartTime = microtime(true); - } - $objects = $this->fetchSlice($queryBuilder, $batchSize, $offset); - if ($loggerClosure) { - $stepNbObjects = count($objects); - } - $objects = array_filter($objects, array($this, 'isObjectIndexable')); - if (!$objects) { - if ($loggerClosure) { - $loggerClosure('Entire batch was filtered away, skipping...'); - } - - continue; - } - - if (!$ignoreErrors) { - $this->objectPersister->insertMany($objects); - } else { - try { - $this->objectPersister->insertMany($objects); - } catch(BulkResponseException $e) { - if ($loggerClosure) { - $loggerClosure(sprintf('%s',$e->getMessage())); - } - } - } - - if ($this->options['clear_object_manager']) { - $manager->clear(); - } - - 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())); - } - } - - if (!$this->options['debug_logging']) { - $this->enableLogging($logger); - } + $this->sliceFetcher = $sliceFetcher; } /** * Counts objects that would be indexed using the query builder. * * @param object $queryBuilder + * * @return integer */ - protected abstract function countObjects($queryBuilder); + abstract protected function countObjects($queryBuilder); /** - * Disables logging and returns the logger that was previously set. + * Creates the query builder, which will be used to fetch objects to index. * - * @return mixed - */ - protected abstract function disableLogging(); - - /** - * Reenables the logger with the previously returned logger from disableLogging(); + * @param string $method * - * @param mixed $logger - * @return mixed + * @return object */ - protected abstract function enableLogging($logger); + abstract protected function createQueryBuilder($method); /** * Fetches a slice of objects using the query builder. @@ -133,14 +69,102 @@ abstract class AbstractProvider extends BaseAbstractProvider * @param object $queryBuilder * @param integer $limit * @param integer $offset + * * @return array */ - protected abstract function fetchSlice($queryBuilder, $limit, $offset); + abstract protected function fetchSlice($queryBuilder, $limit, $offset); /** - * Creates the query builder, which will be used to fetch objects to index. - * - * @return object + * {@inheritDoc} */ - protected abstract function createQueryBuilder(); + protected function doPopulate($options, \Closure $loggerClosure = null) + { + $manager = $this->managerRegistry->getManagerForClass($this->objectClass); + + $queryBuilder = $this->createQueryBuilder($options['query_builder_method']); + $nbObjects = $this->countObjects($queryBuilder); + $offset = $options['offset']; + + $objects = array(); + for (; $offset < $nbObjects; $offset += $options['batch_size']) { + try { + $objects = $this->getSlice($queryBuilder, $options['batch_size'], $offset, $objects); + $objects = $this->filterObjects($options, $objects); + + if (!empty($objects)) { + $this->objectPersister->insertMany($objects); + } + } catch (BulkResponseException $e) { + if (!$options['ignore_errors']) { + throw $e; + } + + if (null !== $loggerClosure) { + $loggerClosure( + $options['batch_size'], + $nbObjects, + sprintf('%s', $e->getMessage()) + ); + } + } + + if ($options['clear_object_manager']) { + $manager->clear(); + } + + usleep($options['sleep']); + + if (null !== $loggerClosure) { + $loggerClosure($options['batch_size'], $nbObjects); + } + } + } + + /** + * {@inheritDoc} + */ + protected function configureOptions() + { + parent::configureOptions(); + + $this->resolver->setDefaults(array( + 'clear_object_manager' => true, + 'debug_logging' => false, + 'ignore_errors' => false, + 'offset' => 0, + 'query_builder_method' => 'createQueryBuilder', + 'sleep' => 0 + )); + } + + /** + * 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 + */ + private 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 + ); + } } diff --git a/Doctrine/Listener.php b/Doctrine/Listener.php index 73a271d..a1d3585 100644 --- a/Doctrine/Listener.php +++ b/Doctrine/Listener.php @@ -2,11 +2,11 @@ namespace FOS\ElasticaBundle\Doctrine; -use Doctrine\Common\EventArgs; -use Doctrine\Common\EventSubscriber; -use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; +use Doctrine\Common\Persistence\Event\LifecycleEventArgs; use FOS\ElasticaBundle\Persister\ObjectPersister; +use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; use FOS\ElasticaBundle\Provider\IndexableInterface; +use Psr\Log\LoggerInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -14,49 +14,52 @@ 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 + * Object persister. * - * @var ObjectPersister + * @var ObjectPersisterInterface */ protected $objectPersister; /** - * List of subscribed events + * Configuration for the listener. * * @var array */ - protected $events; - - /** - * Configuration for the listener - * - * @var string - */ private $config; /** - * Objects scheduled for insertion and replacement + * Objects scheduled for insertion. + * + * @var array */ public $scheduledForInsertion = array(); + + /** + * Objects scheduled to be updated or removed. + * + * @var array + */ public $scheduledForUpdate = array(); /** - * IDs of objects scheduled for removal + * IDs of objects scheduled for removal. + * + * @var array */ public $scheduledForDeletion = array(); /** - * PropertyAccessor instance + * PropertyAccessor instance. * * @var PropertyAccessorInterface */ protected $propertyAccessor; /** - * @var \FOS\ElasticaBundle\Provider\IndexableInterface + * @var IndexableInterface */ private $indexable; @@ -64,71 +67,50 @@ class Listener implements EventSubscriber * Constructor. * * @param ObjectPersisterInterface $objectPersister - * @param array $events - * @param IndexableInterface $indexable - * @param array $config - * @param null $logger + * @param IndexableInterface $indexable + * @param array $config + * @param LoggerInterface $logger */ public function __construct( ObjectPersisterInterface $objectPersister, - array $events, IndexableInterface $indexable, array $config = array(), - $logger = null + LoggerInterface $logger = null ) { $this->config = array_merge(array( 'identifier' => 'id', ), $config); - $this->events = $events; $this->indexable = $indexable; $this->objectPersister = $objectPersister; $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); - if ($logger) { + if ($logger && $this->objectPersister instanceof ObjectPersister) { $this->objectPersister->setLogger($logger); } } /** - * @see Doctrine\Common\EventSubscriber::getSubscribedEvents() - */ - public function getSubscribedEvents() - { - return $this->events; - } - - /** - * Provides unified method for retrieving a doctrine object from an EventArgs instance + * Looks for new objects that should be indexed. * - * @param EventArgs $eventArgs - * @return object Entity | Document - * @throws \RuntimeException if no valid getter is found. + * @param LifecycleEventArgs $eventArgs */ - private function getDoctrineObject(EventArgs $eventArgs) + public function postPersist(LifecycleEventArgs $eventArgs) { - if (method_exists($eventArgs, 'getObject')) { - return $eventArgs->getObject(); - } elseif (method_exists($eventArgs, 'getEntity')) { - return $eventArgs->getEntity(); - } elseif (method_exists($eventArgs, 'getDocument')) { - return $eventArgs->getDocument(); - } - - throw new \RuntimeException('Unable to retrieve object from EventArgs.'); - } - - public function postPersist(EventArgs $eventArgs) - { - $entity = $this->getDoctrineObject($eventArgs); + $entity = $eventArgs->getObject(); if ($this->objectPersister->handlesObject($entity) && $this->isObjectIndexable($entity)) { $this->scheduledForInsertion[] = $entity; } } - public function postUpdate(EventArgs $eventArgs) + /** + * Looks for objects being updated that should be indexed or removed from the index. + * + * @param LifecycleEventArgs $eventArgs + */ + public function postUpdate(LifecycleEventArgs $eventArgs) { - $entity = $this->getDoctrineObject($eventArgs); + $entity = $eventArgs->getObject(); if ($this->objectPersister->handlesObject($entity)) { if ($this->isObjectIndexable($entity)) { @@ -142,11 +124,13 @@ class Listener implements EventSubscriber /** * Delete objects preRemove instead of postRemove so that we have access to the id. Because this is called - * preRemove, first check that the entity is managed by Doctrine + * preRemove, first check that the entity is managed by Doctrine. + * + * @param LifecycleEventArgs $eventArgs */ - public function preRemove(EventArgs $eventArgs) + public function preRemove(LifecycleEventArgs $eventArgs) { - $entity = $this->getDoctrineObject($eventArgs); + $entity = $eventArgs->getObject(); if ($this->objectPersister->handlesObject($entity)) { $this->scheduleForDeletion($entity); @@ -155,7 +139,7 @@ class Listener implements EventSubscriber /** * Persist scheduled objects to ElasticSearch - * After persisting, clear the scheduled queue to prevent multiple data updates when using multiple flush calls + * After persisting, clear the scheduled queue to prevent multiple data updates when using multiple flush calls. */ private function persistScheduled() { @@ -174,29 +158,36 @@ class Listener implements EventSubscriber } /** - * Iterate through scheduled actions before flushing to emulate 2.x behavior. Note that the ElasticSearch index - * will fall out of sync with the source data in the event of a crash during flush. + * Iterate through scheduled actions before flushing to emulate 2.x behavior. + * Note that the ElasticSearch index will fall out of sync with the source + * data in the event of a crash during flush. + * + * This method is only called in legacy configurations of the listener. + * + * @deprecated This method should only be called in applications that depend + * on the behaviour that entities are indexed regardless of if a + * flush is successful. */ - public function preFlush(EventArgs $eventArgs) + public function preFlush() { $this->persistScheduled(); } /** - * Iterating through scheduled actions *after* flushing ensures that the ElasticSearch index will be affected - * only if the query is successful + * Iterating through scheduled actions *after* flushing ensures that the + * ElasticSearch index will be affected only if the query is successful. */ - public function postFlush(EventArgs $eventArgs) + public function postFlush() { $this->persistScheduled(); } /** * Record the specified identifier to delete. Do not need to entire object. - * @param mixed $object - * @return mixed + * + * @param object $object */ - protected function scheduleForDeletion($object) + private function scheduleForDeletion($object) { if ($identifierValue = $this->propertyAccessor->getValue($object, $this->config['identifier'])) { $this->scheduledForDeletion[] = $identifierValue; @@ -207,6 +198,7 @@ class Listener implements EventSubscriber * Checks if the object is indexable or not. * * @param object $object + * * @return bool */ private function isObjectIndexable($object) diff --git a/Doctrine/MongoDB/ElasticaToModelTransformer.php b/Doctrine/MongoDB/ElasticaToModelTransformer.php index cea737f..23a8292 100644 --- a/Doctrine/MongoDB/ElasticaToModelTransformer.php +++ b/Doctrine/MongoDB/ElasticaToModelTransformer.php @@ -7,15 +7,16 @@ use FOS\ElasticaBundle\Doctrine\AbstractElasticaToModelTransformer; /** * Maps Elastica documents with Doctrine objects * This mapper assumes an exact match between - * elastica documents ids and doctrine object ids + * elastica documents ids and doctrine object ids. */ class ElasticaToModelTransformer extends AbstractElasticaToModelTransformer { /** - * Fetch objects for theses identifier values + * Fetch objects for theses identifier values. + * + * @param array $identifierValues ids values + * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays * - * @param array $identifierValues ids values - * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays * @return array of objects or arrays */ protected function findByIdentifiers(array $identifierValues, $hydrate) diff --git a/Doctrine/MongoDB/Provider.php b/Doctrine/MongoDB/Provider.php index 9e1c5dd..e4b08c5 100644 --- a/Doctrine/MongoDB/Provider.php +++ b/Doctrine/MongoDB/Provider.php @@ -27,9 +27,10 @@ class Provider extends AbstractProvider } /** - * Reenables the logger with the previously returned logger from disableLogging(); + * Reenables the logger with the previously returned logger from disableLogging();. * * @param mixed $logger + * * @return mixed */ protected function enableLogging($logger) @@ -43,7 +44,7 @@ class Provider extends AbstractProvider } /** - * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::countObjects() + * {@inheritDoc} */ protected function countObjects($queryBuilder) { @@ -57,7 +58,7 @@ class Provider extends AbstractProvider } /** - * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::fetchSlice() + * {@inheritDoc} */ protected function fetchSlice($queryBuilder, $limit, $offset) { @@ -66,21 +67,21 @@ class Provider extends AbstractProvider } return $queryBuilder - ->limit($limit) ->skip($offset) + ->limit($limit) ->getQuery() ->execute() ->toArray(); } /** - * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::createQueryBuilder() + * {@inheritDoc} */ - protected function createQueryBuilder() + protected function createQueryBuilder($method) { return $this->managerRegistry ->getManagerForClass($this->objectClass) ->getRepository($this->objectClass) - ->{$this->options['query_builder_method']}(); + ->{$method}(); } } diff --git a/Doctrine/MongoDB/SliceFetcher.php b/Doctrine/MongoDB/SliceFetcher.php new file mode 100644 index 0000000..4723da6 --- /dev/null +++ b/Doctrine/MongoDB/SliceFetcher.php @@ -0,0 +1,44 @@ + + */ +class SliceFetcher implements SliceFetcherInterface +{ + /** + * {@inheritdoc} + */ + public function fetch($queryBuilder, $limit, $offset, array $previousSlice, array $identifierFieldNames) + { + if (!$queryBuilder instanceof Builder) { + throw new InvalidArgumentTypeException($queryBuilder, 'Doctrine\ODM\MongoDB\Query\Builder'); + } + + $lastObject = array_pop($previousSlice); + + if ($lastObject) { + $queryBuilder + ->field('_id')->gt($lastObject->getId()) + ->skip(0) + ; + } else { + $queryBuilder->skip($offset); + } + + return $queryBuilder + ->limit($limit) + ->sort(array('_id' => 'asc')) + ->getQuery() + ->execute() + ->toArray() + ; + } +} diff --git a/Doctrine/ORM/ElasticaToModelTransformer.php b/Doctrine/ORM/ElasticaToModelTransformer.php index a57a84c..21d8640 100644 --- a/Doctrine/ORM/ElasticaToModelTransformer.php +++ b/Doctrine/ORM/ElasticaToModelTransformer.php @@ -8,17 +8,18 @@ use Doctrine\ORM\Query; /** * Maps Elastica documents with Doctrine objects * This mapper assumes an exact match between - * elastica documents ids and doctrine object ids + * elastica documents ids and doctrine object ids. */ class ElasticaToModelTransformer extends AbstractElasticaToModelTransformer { const ENTITY_ALIAS = 'o'; /** - * Fetch objects for theses identifier values + * Fetch objects for theses identifier values. + * + * @param array $identifierValues ids values + * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays * - * @param array $identifierValues ids values - * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays * @return array of objects or arrays */ protected function findByIdentifiers(array $identifierValues, $hydrate) @@ -36,7 +37,7 @@ class ElasticaToModelTransformer extends AbstractElasticaToModelTransformer } /** - * Retrieves a query builder to be used for querying by identifiers + * Retrieves a query builder to be used for querying by identifiers. * * @return \Doctrine\ORM\QueryBuilder */ diff --git a/Doctrine/ORM/Provider.php b/Doctrine/ORM/Provider.php index dfd6700..85b5279 100644 --- a/Doctrine/ORM/Provider.php +++ b/Doctrine/ORM/Provider.php @@ -3,7 +3,6 @@ namespace FOS\ElasticaBundle\Doctrine\ORM; use Doctrine\ORM\QueryBuilder; -use Elastica\Exception\Bulk\ResponseException as BulkResponseException; use FOS\ElasticaBundle\Doctrine\AbstractProvider; use FOS\ElasticaBundle\Exception\InvalidArgumentTypeException; @@ -30,9 +29,10 @@ class Provider extends AbstractProvider } /** - * Reenables the logger with the previously returned logger from disableLogging(); + * Reenables the logger with the previously returned logger from disableLogging();. * * @param mixed $logger + * * @return mixed */ protected function enableLogging($logger) @@ -46,7 +46,7 @@ class Provider extends AbstractProvider } /** - * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::countObjects() + * {@inheritDoc} */ protected function countObjects($queryBuilder) { @@ -69,7 +69,9 @@ class Provider extends AbstractProvider } /** - * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::fetchSlice() + * This method should remain in sync with SliceFetcher::fetch until it is deprecated and removed. + * + * {@inheritDoc} */ protected function fetchSlice($queryBuilder, $limit, $offset) { @@ -77,8 +79,8 @@ class Provider extends AbstractProvider throw new InvalidArgumentTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder'); } - /** - * An orderBy DQL part is required to avoid feching the same row twice. + /* + * An orderBy DQL part is required to avoid fetching the same row twice. * @see http://stackoverflow.com/questions/6314879/does-limit-offset-length-require-order-by-for-pagination * @see http://www.postgresql.org/docs/current/static/queries-limit.html * @see http://www.sqlite.org/lang_select.html#orderby @@ -103,14 +105,14 @@ class Provider extends AbstractProvider } /** - * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::createQueryBuilder() + * {@inheritDoc} */ - protected function createQueryBuilder() + protected function createQueryBuilder($method) { return $this->managerRegistry ->getManagerForClass($this->objectClass) ->getRepository($this->objectClass) // ORM query builders require an alias argument - ->{$this->options['query_builder_method']}(static::ENTITY_ALIAS); + ->{$method}(static::ENTITY_ALIAS); } } diff --git a/Doctrine/ORM/SliceFetcher.php b/Doctrine/ORM/SliceFetcher.php new file mode 100644 index 0000000..ac6c816 --- /dev/null +++ b/Doctrine/ORM/SliceFetcher.php @@ -0,0 +1,50 @@ + + */ +class SliceFetcher implements SliceFetcherInterface +{ + /** + * This method should remain in sync with Provider::fetchSlice until that method is deprecated and + * removed. + * + * {@inheritdoc} + */ + public function fetch($queryBuilder, $limit, $offset, array $previousSlice, array $identifierFieldNames) + { + if (!$queryBuilder instanceof QueryBuilder) { + throw new InvalidArgumentTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder'); + } + + /* + * An orderBy DQL part is required to avoid feching the same row twice. + * @see http://stackoverflow.com/questions/6314879/does-limit-offset-length-require-order-by-for-pagination + * @see http://www.postgresql.org/docs/current/static/queries-limit.html + * @see http://www.sqlite.org/lang_select.html#orderby + */ + $orderBy = $queryBuilder->getDQLPart('orderBy'); + if (empty($orderBy)) { + $rootAliases = $queryBuilder->getRootAliases(); + + foreach ($identifierFieldNames as $fieldName) { + $queryBuilder->addOrderBy($rootAliases[0].'.'.$fieldName); + } + } + + return $queryBuilder + ->setFirstResult($offset) + ->setMaxResults($limit) + ->getQuery() + ->getResult() + ; + } +} diff --git a/Doctrine/RepositoryManager.php b/Doctrine/RepositoryManager.php index f8867eb..0d20f64 100644 --- a/Doctrine/RepositoryManager.php +++ b/Doctrine/RepositoryManager.php @@ -25,7 +25,7 @@ class RepositoryManager extends BaseManager } /** - * Return repository for entity + * Return repository for entity. * * Returns custom repository if one specified otherwise * returns a basic repository. @@ -35,7 +35,7 @@ class RepositoryManager extends BaseManager $realEntityName = $entityName; if (strpos($entityName, ':') !== false) { list($namespaceAlias, $simpleClassName) = explode(':', $entityName); - $realEntityName = $this->managerRegistry->getAliasNamespace($namespaceAlias) . '\\' . $simpleClassName; + $realEntityName = $this->managerRegistry->getAliasNamespace($namespaceAlias).'\\'.$simpleClassName; } return parent::getRepository($realEntityName); diff --git a/Doctrine/SliceFetcherInterface.php b/Doctrine/SliceFetcherInterface.php new file mode 100644 index 0000000..a028abf --- /dev/null +++ b/Doctrine/SliceFetcherInterface.php @@ -0,0 +1,24 @@ + + */ +interface SliceFetcherInterface +{ + /** + * Fetches a slice of objects using the query builder. + * + * @param object $queryBuilder + * @param integer $limit + * @param integer $offset + * @param array $previousSlice + * @param array $identifierFieldNames + * + * @return array + */ + public function fetch($queryBuilder, $limit, $offset, array $previousSlice, array $identifierFieldNames); +} diff --git a/Elastica/Client.php b/Elastica/Client.php index 1131ba5..c244eb4 100644 --- a/Elastica/Client.php +++ b/Elastica/Client.php @@ -23,14 +23,19 @@ class Client extends BaseClient private $indexCache = array(); /** - * Symfony's debugging Stopwatch + * Symfony's debugging Stopwatch. * * @var Stopwatch|null */ private $stopwatch; /** - * {@inheritdoc} + * @param string $path + * @param string $method + * @param array $data + * @param array $query + * + * @return \Elastica\Response */ public function request($path, $method = Request::GET, $data = array(), array $query = array()) { @@ -41,20 +46,7 @@ class Client extends BaseClient $start = microtime(true); $response = parent::request($path, $method, $data, $query); - if ($this->_logger and $this->_logger instanceof ElasticaLogger) { - $time = microtime(true) - $start; - - $connection = $this->getLastRequest()->getConnection(); - - $connection_array = array( - 'host' => $connection->getHost(), - 'port' => $connection->getPort(), - 'transport' => $connection->getTransport(), - 'headers' => $connection->hasConfig('headers') ? $connection->getConfig('headers') : array(), - ); - - $this->_logger->logQuery($path, $method, $data, $time, $connection_array, $query); - } + $this->logQuery($path, $method, $data, $query, $start); if ($this->stopwatch) { $this->stopwatch->stop('es_request'); @@ -81,4 +73,32 @@ class Client extends BaseClient { $this->stopwatch = $stopwatch; } + + /** + * Log the query if we have an instance of ElasticaLogger. + * + * @param string $path + * @param string $method + * @param array $data + * @param array $query + * @param int $start + */ + private function logQuery($path, $method, $data, array $query, $start) + { + if (!$this->_logger or !$this->_logger instanceof ElasticaLogger) { + return; + } + + $time = microtime(true) - $start; + $connection = $this->getLastRequest()->getConnection(); + + $connection_array = array( + 'host' => $connection->getHost(), + 'port' => $connection->getPort(), + 'transport' => $connection->getTransport(), + 'headers' => $connection->hasConfig('headers') ? $connection->getConfig('headers') : array(), + ); + + $this->_logger->logQuery($path, $method, $data, $time, $connection_array, $query); + } } diff --git a/Elastica/Index.php b/Elastica/Index.php index bf37c51..ad3bd4d 100644 --- a/Elastica/Index.php +++ b/Elastica/Index.php @@ -3,7 +3,6 @@ namespace FOS\ElasticaBundle\Elastica; use Elastica\Index as BaseIndex; -use Elastica\Type; /** * Overridden Elastica Index class that provides dynamic index name changes. @@ -32,6 +31,9 @@ class Index extends BaseIndex return $this->originalName ?: $this->_name; } + /** + * @param string $type + */ public function getType($type) { if (isset($this->typeCache[$type])) { @@ -42,14 +44,12 @@ class Index extends BaseIndex } /** - * Reassign index name + * Reassign index name. * * While it's technically a regular setter for name property, it's specifically named overrideName, but not setName * since it's used for a very specific case and normally should not be used * * @param string $name Index name - * - * @return void */ public function overrideName($name) { diff --git a/Event/IndexEvent.php b/Event/IndexEvent.php new file mode 100644 index 0000000..ed71d78 --- /dev/null +++ b/Event/IndexEvent.php @@ -0,0 +1,38 @@ + + * + * 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 IndexEvent extends Event +{ + /** + * @var string + */ + private $index; + + /** + * @param string $index + */ + public function __construct($index) + { + $this->index = $index; + } + + /** + * @return string + */ + public function getIndex() + { + return $this->index; + } +} diff --git a/Event/IndexPopulateEvent.php b/Event/IndexPopulateEvent.php new file mode 100644 index 0000000..b35105a --- /dev/null +++ b/Event/IndexPopulateEvent.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Event; + +/** + * Index Populate Event. + * + * @author Oleg Andreyev + */ +class IndexPopulateEvent extends IndexEvent +{ + const PRE_INDEX_POPULATE = 'elastica.index.index_pre_populate'; + const POST_INDEX_POPULATE = 'elastica.index.index_post_populate'; + + /** + * @var bool + */ + private $reset; + + /** + * @var array + */ + private $options; + + /** + * @param string $index + * @param boolean $reset + * @param array $options + */ + public function __construct($index, $reset, $options) + { + parent::__construct($index); + + $this->reset = $reset; + $this->options = $options; + } + + /** + * @return boolean + */ + public function isReset() + { + return $this->reset; + } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * @param boolean $reset + */ + public function setReset($reset) + { + $this->reset = $reset; + } +} diff --git a/Event/IndexResetEvent.php b/Event/IndexResetEvent.php new file mode 100644 index 0000000..871915a --- /dev/null +++ b/Event/IndexResetEvent.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Event; + +/** + * Index ResetEvent. + * + * @author Oleg Andreyev + */ +class IndexResetEvent extends IndexEvent +{ + const PRE_INDEX_RESET = 'elastica.index.pre_reset'; + const POST_INDEX_RESET = 'elastica.index.post_reset'; + + /** + * @var bool + */ + private $force; + + /** + * @var bool + */ + private $populating; + + /** + * @param string $index + * @param bool $populating + * @param bool $force + */ + public function __construct($index, $populating, $force) + { + parent::__construct($index); + + $this->force = $force; + $this->populating = $populating; + } + + /** + * @return boolean + */ + public function isForce() + { + return $this->force; + } + + /** + * @return boolean + */ + public function isPopulating() + { + return $this->populating; + } + + /** + * @param boolean $force + */ + public function setForce($force) + { + $this->force = $force; + } +} diff --git a/Event/TransformEvent.php b/Event/TransformEvent.php new file mode 100644 index 0000000..4f6871f --- /dev/null +++ b/Event/TransformEvent.php @@ -0,0 +1,78 @@ + + * + * 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; + } +} diff --git a/Event/TypePopulateEvent.php b/Event/TypePopulateEvent.php new file mode 100644 index 0000000..dd744f5 --- /dev/null +++ b/Event/TypePopulateEvent.php @@ -0,0 +1,51 @@ + + * + * 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; + +/** + * Type Populate Event. + * + * @author Oleg Andreyev + */ +class TypePopulateEvent extends IndexPopulateEvent +{ + const PRE_TYPE_POPULATE = 'elastica.index.type_pre_populate'; + const POST_TYPE_POPULATE = 'elastica.index.type_post_populate'; + + /** + * @var string + */ + private $type; + + /** + * @param string $index + * @param string $type + * @param bool $reset + * @param array $options + */ + public function __construct($index, $type, $reset, $options) + { + parent::__construct($index, $reset, $options); + + $this->type = $type; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } +} diff --git a/Event/TypeResetEvent.php b/Event/TypeResetEvent.php new file mode 100644 index 0000000..98fa2f4 --- /dev/null +++ b/Event/TypeResetEvent.php @@ -0,0 +1,49 @@ + + * + * 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; + +/** + * Type ResetEvent. + * + * @author Oleg Andreyev + */ +class TypeResetEvent extends IndexEvent +{ + const PRE_TYPE_RESET = 'elastica.index.type_pre_reset'; + const POST_TYPE_RESET = 'elastica.index.type_post_reset'; + + /** + * @var string + */ + private $type; + + /** + * @param string $index + * @param string $type + */ + public function __construct($index, $type) + { + parent::__construct($index); + + $this->type = $type; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } +} diff --git a/Exception/AliasIsIndexException.php b/Exception/AliasIsIndexException.php new file mode 100644 index 0000000..9af6ee3 --- /dev/null +++ b/Exception/AliasIsIndexException.php @@ -0,0 +1,11 @@ +result; } -} \ No newline at end of file +} diff --git a/Index/AliasProcessor.php b/Index/AliasProcessor.php index 93877cd..d8f608d 100644 --- a/Index/AliasProcessor.php +++ b/Index/AliasProcessor.php @@ -11,10 +11,12 @@ namespace FOS\ElasticaBundle\Index; +use Elastica\Client; use Elastica\Exception\ExceptionInterface; +use Elastica\Request; use FOS\ElasticaBundle\Configuration\IndexConfig; -use FOS\ElasticaBundle\Elastica\Client; use FOS\ElasticaBundle\Elastica\Index; +use FOS\ElasticaBundle\Exception\AliasIsIndexException; class AliasProcessor { @@ -22,115 +24,174 @@ class AliasProcessor * Sets the randomised root name for an index. * * @param IndexConfig $indexConfig - * @param Index $index + * @param Index $index */ public function setRootName(IndexConfig $indexConfig, Index $index) { - $index->overrideName(sprintf('%s_%s', $indexConfig->getElasticSearchName(), uniqid())); + $index->overrideName( + sprintf('%s_%s', + $indexConfig->getElasticSearchName(), + date('Y-m-d-His') + ) + ); } /** * Switches an index to become the new target for an alias. Only applies for * indexes that are set to use aliases. * + * $force will delete an index encountered where an alias is expected. + * * @param IndexConfig $indexConfig - * @param Index $index + * @param Index $index + * @param bool $force + * + * @throws AliasIsIndexException * @throws \RuntimeException */ - public function switchIndexAlias(IndexConfig $indexConfig, Index $index) + public function switchIndexAlias(IndexConfig $indexConfig, Index $index, $force = false) { $client = $index->getClient(); $aliasName = $indexConfig->getElasticSearchName(); - $oldIndexName = false; + $oldIndexName = null; $newIndexName = $index->getName(); - $aliasedIndexes = $this->getAliasedIndexes($client, $aliasName); + try { + $oldIndexName = $this->getAliasedIndex($client, $aliasName); + } catch (AliasIsIndexException $e) { + if (!$force) { + throw $e; + } - if (count($aliasedIndexes) > 1) { - throw new \RuntimeException( - sprintf( - 'Alias %s is used for multiple indexes: [%s]. - Make sure it\'s either not used or is assigned to one index only', - $aliasName, - join(', ', $aliasedIndexes) - ) - ); + $this->deleteIndex($client, $aliasName); } + try { + $aliasUpdateRequest = $this->buildAliasUpdateRequest($oldIndexName, $aliasName, $newIndexName); + $client->request('_aliases', 'POST', $aliasUpdateRequest); + } catch (ExceptionInterface $e) { + $this->cleanupRenameFailure($client, $newIndexName, $e); + } + + // Delete the old index after the alias has been switched + if (null !== $oldIndexName) { + $this->deleteIndex($client, $oldIndexName); + } + } + + /** + * Builds an ElasticSearch request to rename or create an alias. + * + * @param string|null $aliasedIndex + * @param string $aliasName + * @param string $newIndexName + * @return array + */ + private function buildAliasUpdateRequest($aliasedIndex, $aliasName, $newIndexName) + { $aliasUpdateRequest = array('actions' => array()); - if (count($aliasedIndexes) == 1) { + if (null !== $aliasedIndex) { // if the alias is set - add an action to remove it - $oldIndexName = $aliasedIndexes[0]; $aliasUpdateRequest['actions'][] = array( - 'remove' => array('index' => $oldIndexName, 'alias' => $aliasName) + 'remove' => array('index' => $aliasedIndex, 'alias' => $aliasName), ); } // add an action to point the alias to the new index $aliasUpdateRequest['actions'][] = array( - 'add' => array('index' => $newIndexName, 'alias' => $aliasName) + 'add' => array('index' => $newIndexName, 'alias' => $aliasName), ); - try { - $client->request('_aliases', 'POST', $aliasUpdateRequest); - } catch (ExceptionInterface $renameAliasException) { - $additionalError = ''; - // if we failed to move the alias, delete the newly built index - try { - $index->delete(); - } catch (ExceptionInterface $deleteNewIndexException) { - $additionalError = sprintf( - 'Tried to delete newly built index %s, but also failed: %s', - $newIndexName, - $deleteNewIndexException->getMessage() - ); - } + return $aliasUpdateRequest; + } - throw new \RuntimeException( - sprintf( - 'Failed to updated index alias: %s. %s', - $renameAliasException->getMessage(), - $additionalError ?: sprintf('Newly built index %s was deleted', $newIndexName) - ), 0, $renameAliasException + /** + * Cleans up an index when we encounter a failure to rename the alias. + * + * @param Client $client + * @param string $indexName + * @param \Exception $renameAliasException + */ + private function cleanupRenameFailure(Client $client, $indexName, \Exception $renameAliasException) + { + $additionalError = ''; + try { + $this->deleteIndex($client, $indexName); + } catch (ExceptionInterface $deleteNewIndexException) { + $additionalError = sprintf( + 'Tried to delete newly built index %s, but also failed: %s', + $indexName, + $deleteNewIndexException->getMessage() ); } - // Delete the old index after the alias has been switched - if ($oldIndexName) { - $oldIndex = new Index($client, $oldIndexName); - try { - $oldIndex->delete(); - } catch (ExceptionInterface $deleteOldIndexException) { - throw new \RuntimeException( - sprintf( - 'Failed to delete old index %s with message: %s', - $oldIndexName, - $deleteOldIndexException->getMessage() - ), 0, $deleteOldIndexException - ); - } + throw new \RuntimeException(sprintf( + 'Failed to updated index alias: %s. %s', + $renameAliasException->getMessage(), + $additionalError ?: sprintf('Newly built index %s was deleted', $indexName) + ), 0, $renameAliasException); + } + + /** + * Delete an index. + * + * @param Client $client + * @param string $indexName Index name to delete + */ + private function deleteIndex(Client $client, $indexName) + { + try { + $path = sprintf("%s", $indexName); + $client->request($path, Request::DELETE); + } catch (ExceptionInterface $deleteOldIndexException) { + throw new \RuntimeException(sprintf( + 'Failed to delete index %s with message: %s', + $indexName, + $deleteOldIndexException->getMessage() + ), 0, $deleteOldIndexException); } } /** - * Returns array of indexes which are mapped to given alias + * Returns the name of a single index that an alias points to or throws + * an exception if there is more than one. * + * @param Client $client * @param string $aliasName Alias name - * @return array + * + * @return string|null + * + * @throws AliasIsIndexException */ - private function getAliasedIndexes(Client $client, $aliasName) + private function getAliasedIndex(Client $client, $aliasName) { $aliasesInfo = $client->request('_aliases', 'GET')->getData(); $aliasedIndexes = array(); foreach ($aliasesInfo as $indexName => $indexInfo) { + if ($indexName === $aliasName) { + throw new AliasIsIndexException($indexName); + } + if (!isset($indexInfo['aliases'])) { + continue; + } + $aliases = array_keys($indexInfo['aliases']); if (in_array($aliasName, $aliases)) { $aliasedIndexes[] = $indexName; } } - return $aliasedIndexes; + if (count($aliasedIndexes) > 1) { + throw new \RuntimeException(sprintf( + 'Alias %s is used for multiple indexes: [%s]. Make sure it\'s'. + 'either not used or is assigned to one index only', + $aliasName, + implode(', ', $aliasedIndexes) + )); + } + + return array_shift($aliasedIndexes); } } diff --git a/Index/IndexManager.php b/Index/IndexManager.php index 38249a7..98ce870 100644 --- a/Index/IndexManager.php +++ b/Index/IndexManager.php @@ -6,6 +6,11 @@ use FOS\ElasticaBundle\Elastica\Index; class IndexManager { + /** + * @var Index + */ + private $defaultIndex; + /** * @var array */ @@ -22,7 +27,7 @@ class IndexManager } /** - * Gets all registered indexes + * Gets all registered indexes. * * @return array */ @@ -32,10 +37,12 @@ class IndexManager } /** - * Gets an index by its name + * Gets an index by its name. * * @param string $name Index to return, or the default index if null + * * @return Index + * * @throws \InvalidArgumentException if no index exists for the given name */ public function getIndex($name = null) @@ -52,7 +59,7 @@ class IndexManager } /** - * Gets the default index + * Gets the default index. * * @return Index */ diff --git a/Index/MappingBuilder.php b/Index/MappingBuilder.php index 21ae871..e03bf54 100644 --- a/Index/MappingBuilder.php +++ b/Index/MappingBuilder.php @@ -27,6 +27,7 @@ class MappingBuilder * Builds mappings for an entire index. * * @param IndexConfig $indexConfig + * * @return array */ public function buildIndexMapping(IndexConfig $indexConfig) @@ -37,13 +38,13 @@ class MappingBuilder } $mapping = array(); - if ($typeMappings) { + if (!empty($typeMappings)) { $mapping['mappings'] = $typeMappings; } // 'warmers' => $indexConfig->getWarmers(), $settings = $indexConfig->getSettings(); - if ($settings) { + if (!empty($settings)) { $mapping['settings'] = $settings; } @@ -54,17 +55,24 @@ class MappingBuilder * Builds mappings for a single type. * * @param TypeConfig $typeConfig + * * @return array */ 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(); @@ -87,9 +95,9 @@ class MappingBuilder $mapping['_meta']['model'] = $typeConfig->getModel(); } - if (!$mapping) { + if (empty($mapping)) { // Empty mapping, we want it encoded as a {} instead of a [] - $mapping = new \stdClass; + $mapping = new \stdClass(); } return $mapping; @@ -104,9 +112,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']); } diff --git a/Index/Resetter.php b/Index/Resetter.php index 3f07fa1..2b39157 100644 --- a/Index/Resetter.php +++ b/Index/Resetter.php @@ -5,12 +5,13 @@ namespace FOS\ElasticaBundle\Index; use Elastica\Index; use Elastica\Exception\ResponseException; use Elastica\Type\Mapping; -use FOS\ElasticaBundle\Configuration\IndexConfig; use FOS\ElasticaBundle\Configuration\ConfigManager; -use FOS\ElasticaBundle\Elastica\Client; +use FOS\ElasticaBundle\Event\IndexResetEvent; +use FOS\ElasticaBundle\Event\TypeResetEvent; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** - * Deletes and recreates indexes + * Deletes and recreates indexes. */ class Resetter { @@ -20,10 +21,15 @@ class Resetter private $aliasProcessor; /*** - * @var \FOS\ElasticaBundle\Configuration\Manager + * @var ConfigManager */ private $configManager; + /** + * @var EventDispatcherInterface + */ + private $dispatcher; + /** * @var IndexManager */ @@ -34,21 +40,37 @@ class Resetter */ private $mappingBuilder; - public function __construct(ConfigManager $configManager, IndexManager $indexManager, AliasProcessor $aliasProcessor, MappingBuilder $mappingBuilder) - { + /** + * @param ConfigManager $configManager + * @param IndexManager $indexManager + * @param AliasProcessor $aliasProcessor + * @param MappingBuilder $mappingBuilder + * @param EventDispatcherInterface $eventDispatcher + */ + public function __construct( + ConfigManager $configManager, + IndexManager $indexManager, + AliasProcessor $aliasProcessor, + MappingBuilder $mappingBuilder, + EventDispatcherInterface $eventDispatcher + ) { $this->aliasProcessor = $aliasProcessor; $this->configManager = $configManager; + $this->dispatcher = $eventDispatcher; $this->indexManager = $indexManager; $this->mappingBuilder = $mappingBuilder; } /** - * Deletes and recreates all indexes + * Deletes and recreates all indexes. + * + * @param bool $populating + * @param bool $force */ - public function resetAllIndexes($populating = false) + public function resetAllIndexes($populating = false, $force = false) { foreach ($this->configManager->getIndexNames() as $name) { - $this->resetIndex($name, $populating); + $this->resetIndex($name, $populating, $force); } } @@ -57,14 +79,19 @@ class Resetter * with a randomised name for an alias to be set after population. * * @param string $indexName - * @param bool $populating + * @param bool $populating + * @param bool $force If index exists with same name as alias, remove it + * * @throws \InvalidArgumentException if no index exists for the given name */ - public function resetIndex($indexName, $populating = false) + public function resetIndex($indexName, $populating = false, $force = false) { $indexConfig = $this->configManager->getIndexConfiguration($indexName); $index = $this->indexManager->getIndex($indexName); + $event = new IndexResetEvent($indexName, $populating, $force); + $this->dispatcher->dispatch(IndexResetEvent::PRE_INDEX_RESET, $event); + if ($indexConfig->isUseAlias()) { $this->aliasProcessor->setRootName($indexConfig, $index); } @@ -73,15 +100,18 @@ class Resetter $index->create($mapping, true); if (!$populating and $indexConfig->isUseAlias()) { - $this->aliasProcessor->switchIndexAlias($indexConfig, $index); + $this->aliasProcessor->switchIndexAlias($indexConfig, $index, $force); } + + $this->dispatcher->dispatch(IndexResetEvent::POST_INDEX_RESET, $event); } /** - * Deletes and recreates a mapping type for the named index + * Deletes and recreates a mapping type for the named index. * * @param string $indexName * @param string $typeName + * * @throws \InvalidArgumentException if no index or type mapping exists for the given names * @throws ResponseException */ @@ -90,6 +120,9 @@ class Resetter $typeConfig = $this->configManager->getTypeConfiguration($indexName, $typeName); $type = $this->indexManager->getIndex($indexName)->getType($typeName); + $event = new TypeResetEvent($indexName, $typeName); + $this->dispatcher->dispatch(TypeResetEvent::PRE_TYPE_RESET, $event); + try { $type->delete(); } catch (ResponseException $e) { @@ -98,18 +131,20 @@ class Resetter } } - $mapping = new Mapping; + $mapping = new Mapping(); foreach ($this->mappingBuilder->buildTypeMapping($typeConfig) as $name => $field) { $mapping->setParam($name, $field); } $type->setMapping($mapping); + + $this->dispatcher->dispatch(TypeResetEvent::POST_TYPE_RESET, $event); } /** * A command run when a population has finished. * - * @param $indexName + * @param string $indexName */ public function postPopulate($indexName) { diff --git a/Manager/RepositoryManager.php b/Manager/RepositoryManager.php index be07b42..1a0601c 100644 --- a/Manager/RepositoryManager.php +++ b/Manager/RepositoryManager.php @@ -25,13 +25,13 @@ class RepositoryManager implements RepositoryManagerInterface public function addEntity($entityName, FinderInterface $finder, $repositoryName = null) { - $this->entities[$entityName]= array(); + $this->entities[$entityName] = array(); $this->entities[$entityName]['finder'] = $finder; $this->entities[$entityName]['repositoryName'] = $repositoryName; } /** - * Return repository for entity + * Return repository for entity. * * Returns custom repository if one specified otherwise * returns a basic repository. @@ -63,12 +63,16 @@ class RepositoryManager implements RepositoryManagerInterface if ($annotation) { $this->entities[$entityName]['repositoryName'] = $annotation->repositoryClass; + return $annotation->repositoryClass; } return 'FOS\ElasticaBundle\Repository'; } + /** + * @param string $entityName + */ private function createRepository($entityName) { if (!class_exists($repositoryName = $this->getRepositoryName($entityName))) { diff --git a/Manager/RepositoryManagerInterface.php b/Manager/RepositoryManagerInterface.php index 1008371..0a38e0e 100644 --- a/Manager/RepositoryManagerInterface.php +++ b/Manager/RepositoryManagerInterface.php @@ -12,7 +12,6 @@ use FOS\ElasticaBundle\Finder\FinderInterface; */ interface RepositoryManagerInterface { - /** * Adds entity name and its finder. * Custom repository class name can also be added. @@ -24,7 +23,7 @@ interface RepositoryManagerInterface public function addEntity($entityName, FinderInterface $finder, $repositoryName = null); /** - * Return repository for entity + * Return repository for entity. * * Returns custom repository if one specified otherwise * returns a basic repository. diff --git a/Paginator/FantaPaginatorAdapter.php b/Paginator/FantaPaginatorAdapter.php index 2ad6983..8f9a60a 100644 --- a/Paginator/FantaPaginatorAdapter.php +++ b/Paginator/FantaPaginatorAdapter.php @@ -20,8 +20,6 @@ class FantaPaginatorAdapter implements AdapterInterface * Returns the number of results. * * @return integer The number of results. - * - * @api */ public function getNbResults() { @@ -29,15 +27,25 @@ class FantaPaginatorAdapter implements AdapterInterface } /** - * Returns Facets + * Returns Facets. + * + * @return mixed + */ + public function getFacets() + { + return $this->adapter->getFacets(); + } + + /** + * Returns Aggregations. * * @return mixed * * @api */ - public function getFacets() + public function getAggregations() { - return $this->adapter->getFacets(); + return $this->adapter->getAggregations(); } /** @@ -47,8 +55,6 @@ class FantaPaginatorAdapter implements AdapterInterface * @param integer $length The length. * * @return array|\Traversable The slice. - * - * @api */ public function getSlice($offset, $length) { diff --git a/Paginator/PaginatorAdapterInterface.php b/Paginator/PaginatorAdapterInterface.php index 25786a0..adf7df2 100644 --- a/Paginator/PaginatorAdapterInterface.php +++ b/Paginator/PaginatorAdapterInterface.php @@ -8,10 +8,8 @@ interface PaginatorAdapterInterface * Returns the number of results. * * @return integer The number of results. - * - * @api */ - function getTotalHits(); + public function getTotalHits(); /** * Returns an slice of the results. @@ -20,15 +18,20 @@ interface PaginatorAdapterInterface * @param integer $length The length. * * @return PartialResultsInterface - * - * @api */ - function getResults($offset, $length); + public function getResults($offset, $length); /** - * Returns Facets + * Returns Facets. * * @return mixed */ - function getFacets(); + public function getFacets(); + + /** + * Returns Aggregations. + * + * @return mixed + */ + public function getAggregations(); } diff --git a/Paginator/PartialResultsInterface.php b/Paginator/PartialResultsInterface.php index 9efe7f3..156d27f 100644 --- a/Paginator/PartialResultsInterface.php +++ b/Paginator/PartialResultsInterface.php @@ -8,24 +8,27 @@ interface PartialResultsInterface * Returns the paginated results. * * @return array - * - * @api */ - function toArray(); + public function toArray(); /** * Returns the number of results. * * @return integer The number of results. - * - * @api */ - function getTotalHits(); + public function getTotalHits(); /** - * Returns the facets + * Returns the facets. * * @return array */ - function getFacets(); -} \ No newline at end of file + public function getFacets(); + + /** + * Returns the aggregations. + * + * @return array + */ + public function getAggregations(); +} diff --git a/Paginator/RawPaginatorAdapter.php b/Paginator/RawPaginatorAdapter.php index 9136bc0..2eebde0 100644 --- a/Paginator/RawPaginatorAdapter.php +++ b/Paginator/RawPaginatorAdapter.php @@ -8,7 +8,7 @@ use Elastica\ResultSet; use InvalidArgumentException; /** - * Allows pagination of Elastica\Query. Does not map results + * Allows pagination of Elastica\Query. Does not map results. */ class RawPaginatorAdapter implements PaginatorAdapterInterface { @@ -37,11 +37,16 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface */ private $facets; + /** + * @var array for the aggregations + */ + private $aggregations; + /** * @see PaginatorAdapterInterface::__construct * * @param SearchableInterface $searchable the object to search in - * @param Query $query the query to search + * @param Query $query the query to search * @param array $options */ public function __construct(SearchableInterface $searchable, Query $query, array $options = array()) @@ -54,9 +59,11 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface /** * Returns the paginated results. * - * @param $offset - * @param $itemCountPerPage + * @param integer $offset + * @param integer $itemCountPerPage + * * @throws \InvalidArgumentException + * * @return ResultSet */ protected function getElasticaResults($offset, $itemCountPerPage) @@ -67,7 +74,7 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface ? (integer) $this->query->getParam('size') : null; - if ($size && $size < $offset + $itemCountPerPage) { + if (null !== $size && $size < $offset + $itemCountPerPage) { $itemCountPerPage = $size - $offset; } @@ -82,6 +89,8 @@ 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; } @@ -90,6 +99,7 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface * * @param int $offset * @param int $itemCountPerPage + * * @return PartialResultsInterface */ public function getResults($offset, $itemCountPerPage) @@ -100,27 +110,33 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface /** * Returns the number of results. * + * If genuineTotal is provided as true, total hits is returned from the + * hits.total value from the search results instead of just returning + * the requested size. + * + * @param boolean $genuineTotal + * * @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(); + if (! isset($this->totalHits)) { + $this->totalHits = $this->searchable->count($this->query); } - return $this->query->hasParam('size') + return $this->query->hasParam('size') && !$genuineTotal ? min($this->totalHits, (integer) $this->query->getParam('size')) : $this->totalHits; } /** - * Returns Facets + * Returns Facets. * * @return mixed */ public function getFacets() { - if ( ! isset($this->facets)) { + if (! isset($this->facets)) { $this->facets = $this->searchable->search($this->query)->getFacets(); } @@ -128,7 +144,21 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface } /** - * Returns the Query + * 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. * * @return Query the search query */ diff --git a/Paginator/RawPartialResults.php b/Paginator/RawPartialResults.php index a4afb00..e45c6dd 100644 --- a/Paginator/RawPartialResults.php +++ b/Paginator/RawPartialResults.php @@ -6,7 +6,7 @@ use Elastica\ResultSet; use Elastica\Result; /** - * Raw partial results transforms to a simple array + * Raw partial results transforms to a simple array. */ class RawPartialResults implements PartialResultsInterface { @@ -25,7 +25,7 @@ class RawPartialResults implements PartialResultsInterface */ public function toArray() { - return array_map(function(Result $result) { + return array_map(function (Result $result) { return $result->getSource(); }, $this->resultSet->getResults()); } @@ -47,6 +47,18 @@ class RawPartialResults implements PartialResultsInterface return $this->resultSet->getFacets(); } - return null; + return; } -} \ No newline at end of file + + /** + * {@inheritDoc} + */ + public function getAggregations() + { + if ($this->resultSet->hasAggregations()) { + return $this->resultSet->getAggregations(); + } + + return; + } +} diff --git a/Paginator/TransformedPaginatorAdapter.php b/Paginator/TransformedPaginatorAdapter.php index 3b4716f..bf152fb 100644 --- a/Paginator/TransformedPaginatorAdapter.php +++ b/Paginator/TransformedPaginatorAdapter.php @@ -7,15 +7,15 @@ use Elastica\SearchableInterface; use Elastica\Query; /** - * Allows pagination of \Elastica\Query + * Allows pagination of \Elastica\Query. */ class TransformedPaginatorAdapter extends RawPaginatorAdapter { private $transformer; /** - * @param SearchableInterface $searchable the object to search in - * @param Query $query the query to search + * @param SearchableInterface $searchable the object to search in + * @param Query $query the query to search * @param array $options * @param ElasticaToModelTransformerInterface $transformer the transformer for fetching the results */ diff --git a/Paginator/TransformedPartialResults.php b/Paginator/TransformedPartialResults.php index 13d716c..c9470c3 100644 --- a/Paginator/TransformedPartialResults.php +++ b/Paginator/TransformedPartialResults.php @@ -6,14 +6,14 @@ use FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerInterface; use Elastica\ResultSet; /** - * Partial transformed result set + * Partial transformed result set. */ class TransformedPartialResults extends RawPartialResults { protected $transformer; /** - * @param ResultSet $resultSet + * @param ResultSet $resultSet * @param \FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerInterface $transformer */ public function __construct(ResultSet $resultSet, ElasticaToModelTransformerInterface $transformer) @@ -30,4 +30,4 @@ class TransformedPartialResults extends RawPartialResults { return $this->transformer->transform($this->resultSet->getResults()); } -} \ No newline at end of file +} diff --git a/Persister/ObjectPersister.php b/Persister/ObjectPersister.php index 9604f7e..92dc005 100644 --- a/Persister/ObjectPersister.php +++ b/Persister/ObjectPersister.php @@ -4,14 +4,13 @@ namespace FOS\ElasticaBundle\Persister; use Psr\Log\LoggerInterface; use Elastica\Exception\BulkException; -use Elastica\Exception\NotFoundException; use FOS\ElasticaBundle\Transformer\ModelToElasticaTransformerInterface; use Elastica\Type; use Elastica\Document; /** * Inserts, replaces and deletes single documents in an elastica type - * Accepts domain model objects and converts them to elastica documents + * Accepts domain model objects and converts them to elastica documents. * * @author Thibault Duplessis */ @@ -35,6 +34,7 @@ class ObjectPersister implements ObjectPersisterInterface * If the ObjectPersister handles a given object. * * @param object $object + * * @return bool */ public function handlesObject($object) @@ -42,17 +42,20 @@ class ObjectPersister implements ObjectPersisterInterface return $object instanceof $this->objectClass; } + /** + * @param LoggerInterface $logger + */ public function setLogger(LoggerInterface $logger) { $this->logger = $logger; } /** - * Log exception if logger defined for persister belonging to the current listener, otherwise re-throw + * Log exception if logger defined for persister belonging to the current listener, otherwise re-throw. * * @param BulkException $e + * * @throws BulkException - * @return null */ private function log(BulkException $e) { @@ -65,7 +68,7 @@ class ObjectPersister implements ObjectPersisterInterface /** * Insert one object into the type - * The object will be transformed to an elastica document + * The object will be transformed to an elastica document. * * @param object $object */ @@ -75,10 +78,9 @@ class ObjectPersister implements ObjectPersisterInterface } /** - * Replaces one object in the type + * Replaces one object in the type. * * @param object $object - * @return null **/ public function replaceOne($object) { @@ -86,10 +88,9 @@ class ObjectPersister implements ObjectPersisterInterface } /** - * Deletes one object in the type + * Deletes one object in the type. * * @param object $object - * @return null **/ public function deleteOne($object) { @@ -97,11 +98,9 @@ class ObjectPersister implements ObjectPersisterInterface } /** - * Deletes one object in the type by id + * Deletes one object in the type by id. * * @param mixed $id - * - * @return null **/ public function deleteById($id) { @@ -109,7 +108,7 @@ class ObjectPersister implements ObjectPersisterInterface } /** - * Bulk insert an array of objects in the type for the given method + * Bulk insert an array of objects in the type for the given method. * * @param array $objects array of domain model objects * @param string Method to call @@ -149,7 +148,7 @@ class ObjectPersister implements ObjectPersisterInterface } /** - * Bulk deletes an array of objects in the type + * Bulk deletes an array of objects in the type. * * @param array $objects array of domain model objects */ @@ -167,7 +166,7 @@ class ObjectPersister implements ObjectPersisterInterface } /** - * Bulk deletes records from an array of identifiers + * Bulk deletes records from an array of identifiers. * * @param array $identifiers array of domain model object identifiers */ @@ -181,9 +180,10 @@ class ObjectPersister implements ObjectPersisterInterface } /** - * Transforms an object to an elastica document + * Transforms an object to an elastica document. * * @param object $object + * * @return Document the elastica document */ public function transformToElasticaDocument($object) diff --git a/Persister/ObjectPersisterInterface.php b/Persister/ObjectPersisterInterface.php index 2b4c8ee..f624971 100644 --- a/Persister/ObjectPersisterInterface.php +++ b/Persister/ObjectPersisterInterface.php @@ -4,66 +4,73 @@ namespace FOS\ElasticaBundle\Persister; /** * Inserts, replaces and deletes single documents in an elastica type - * Accepts domain model objects and converts them to elastica documents + * Accepts domain model objects and converts them to elastica documents. * * @author Thibault Duplessis */ interface ObjectPersisterInterface { + /** + * Checks if this persister can handle the given object or not. + * + * @param mixed $object + * + * @return boolean + */ + public function handlesObject($object); + /** * Insert one object into the type - * The object will be transformed to an elastica document + * The object will be transformed to an elastica document. * * @param object $object */ - function insertOne($object); + public function insertOne($object); /** - * Replaces one object in the type + * Replaces one object in the type. * * @param object $object **/ - function replaceOne($object); + public function replaceOne($object); /** - * Deletes one object in the type + * Deletes one object in the type. * * @param object $object **/ - function deleteOne($object); + public function deleteOne($object); /** - * Deletes one object in the type by id + * Deletes one object in the type by id. * * @param mixed $id - * - * @return null */ - function deleteById($id); + public function deleteById($id); /** - * Bulk inserts an array of objects in the type + * Bulk inserts an array of objects in the type. * * @param array $objects array of domain model objects */ - function insertMany(array $objects); + public function insertMany(array $objects); /** - * Bulk updates an array of objects in the type + * Bulk updates an array of objects in the type. * * @param array $objects array of domain model objects */ - function replaceMany(array $objects); + public function replaceMany(array $objects); /** - * Bulk deletes an array of objects in the type + * Bulk deletes an array of objects in the type. * * @param array $objects array of domain model objects */ - function deleteMany(array $objects); + public function deleteMany(array $objects); /** - * Bulk deletes records from an array of identifiers + * Bulk deletes records from an array of identifiers. * * @param array $identifiers array of domain model object identifiers */ diff --git a/Persister/ObjectSerializerPersister.php b/Persister/ObjectSerializerPersister.php index 1a15656..792aa9a 100644 --- a/Persister/ObjectSerializerPersister.php +++ b/Persister/ObjectSerializerPersister.php @@ -9,7 +9,7 @@ use FOS\ElasticaBundle\Transformer\ModelToElasticaTransformerInterface; /** * Inserts, replaces and deletes single objects in an elastica type, making use * of elastica's serializer support to convert objects in to elastica documents. - * Accepts domain model objects and passes them directly to elastica + * Accepts domain model objects and passes them directly to elastica. * * @author Lea Haensenberber */ @@ -17,17 +17,25 @@ class ObjectSerializerPersister extends ObjectPersister { protected $serializer; + /** + * @param Type $type + * @param ModelToElasticaTransformerInterface $transformer + * @param string $objectClass + * @param callable $serializer + */ public function __construct(Type $type, ModelToElasticaTransformerInterface $transformer, $objectClass, $serializer) { parent::__construct($type, $transformer, $objectClass, array()); - $this->serializer = $serializer; + + $this->serializer = $serializer; } /** * Transforms an object to an elastica document - * with just the identifier set + * with just the identifier set. * * @param object $object + * * @return Document the elastica document */ public function transformToElasticaDocument($object) diff --git a/Propel/ElasticaToModelTransformer.php b/Propel/ElasticaToModelTransformer.php index af5f8ab..d143478 100644 --- a/Propel/ElasticaToModelTransformer.php +++ b/Propel/ElasticaToModelTransformer.php @@ -3,6 +3,7 @@ namespace FOS\ElasticaBundle\Propel; use FOS\ElasticaBundle\HybridResult; +use FOS\ElasticaBundle\Transformer\AbstractElasticaToModelTransformer; use FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -14,7 +15,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; * * @author William Durand */ -class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface +class ElasticaToModelTransformer extends AbstractElasticaToModelTransformer { /** * Propel model class to map to Elastica documents. @@ -33,18 +34,11 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface 'identifier' => 'id', ); - /** - * PropertyAccessor instance. - * - * @var PropertyAccessorInterface - */ - protected $propertyAccessor; - /** * Constructor. * * @param string $objectClass - * @param array $options + * @param array $options */ public function __construct($objectClass, array $options = array()) { @@ -52,21 +46,12 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface $this->options = array_merge($this->options, $options); } - /** - * Set the PropertyAccessor instance. - * - * @param PropertyAccessorInterface $propertyAccessor - */ - public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor) - { - $this->propertyAccessor = $propertyAccessor; - } - /** * Transforms an array of Elastica document into an array of Propel entities * fetched from the database. * * @param array $elasticaObjects + * * @return array|\ArrayObject */ public function transform(array $elasticaObjects) @@ -81,11 +66,7 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface // Sort objects in the order of their IDs $idPos = array_flip($ids); $identifier = $this->options['identifier']; - $propertyAccessor = $this->propertyAccessor; - - $sortCallback = function($a, $b) use ($idPos, $identifier, $propertyAccessor) { - return $idPos[$propertyAccessor->getValue($a, $identifier)] > $idPos[$propertyAccessor->getValue($b, $identifier)]; - }; + $sortCallback = $this->getSortingClosure($idPos, $identifier); if (is_object($objects)) { $objects->uasort($sortCallback); @@ -104,7 +85,7 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface $objects = $this->transform($elasticaObjects); $result = array(); - for ($i = 0; $i < count($elasticaObjects); $i++) { + for ($i = 0, $j = count($elasticaObjects); $i < $j; $i++) { $result[] = new HybridResult($elasticaObjects[$i], $objects[$i]); } @@ -135,6 +116,7 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface * * @param array $identifierValues Identifier values * @param boolean $hydrate Whether or not to hydrate the results + * * @return array */ protected function findByIdentifiers(array $identifierValues, $hydrate) @@ -145,7 +127,7 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface $query = $this->createQuery($this->objectClass, $this->options['identifier'], $identifierValues); - if ( ! $hydrate) { + if (! $hydrate) { return $query->toArray(); } @@ -158,6 +140,7 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface * @param string $class Propel model class * @param string $identifierField Identifier field name (e.g. "id") * @param array $identifierValues Identifier values + * * @return \ModelCriteria */ protected function createQuery($class, $identifierField, array $identifierValues) @@ -170,6 +153,8 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface /** * @see https://github.com/doctrine/common/blob/master/lib/Doctrine/Common/Util/Inflector.php + * + * @param string $str */ private function camelize($str) { diff --git a/Propel/Provider.php b/Propel/Provider.php index 38f7a61..9864d53 100644 --- a/Propel/Provider.php +++ b/Propel/Provider.php @@ -5,53 +5,69 @@ namespace FOS\ElasticaBundle\Propel; use FOS\ElasticaBundle\Provider\AbstractProvider; /** - * Propel provider + * Propel provider. * * @author William Durand */ class Provider extends AbstractProvider { /** - * @see FOS\ElasticaBundle\Provider\ProviderInterface::populate() + * {@inheritDoc} */ - public function populate(\Closure $loggerClosure = null, array $options = array()) + public function doPopulate($options, \Closure $loggerClosure = null) { - $queryClass = $this->objectClass . 'Query'; + $queryClass = $this->objectClass.'Query'; $nbObjects = $queryClass::create()->count(); - $offset = isset($options['offset']) ? intval($options['offset']) : 0; - $sleep = isset($options['sleep']) ? intval($options['sleep']) : 0; - $batchSize = isset($options['batch-size']) ? intval($options['batch-size']) : $this->options['batch_size']; - for (; $offset < $nbObjects; $offset += $batchSize) { - if ($loggerClosure) { - $stepStartTime = microtime(true); - } + $offset = $options['offset']; + for (; $offset < $nbObjects; $offset += $options['batch_size']) { $objects = $queryClass::create() - ->limit($batchSize) + ->limit($options['batch_size']) ->offset($offset) ->find() ->getArrayCopy(); - if ($loggerClosure) { - $stepNbObjects = count($objects); - } - $objects = array_filter($objects, array($this, 'isObjectIndexable')); - if (!$objects) { - $loggerClosure('Entire batch was filtered away, skipping...'); - - continue; + $objects = $this->filterObjects($options, $objects); + if (!empty($objects)) { + $this->objectPersister->insertMany($objects); } - $this->objectPersister->insertMany($objects); - - usleep($sleep); + usleep($options['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($options['batch_size'], $nbObjects); } } } + + /** + * {@inheritDoc} + */ + protected function configureOptions() + { + parent::configureOptions(); + + $this->resolver->setDefaults(array( + 'clear_object_manager' => true, + 'debug_logging' => false, + 'ignore_errors' => false, + 'offset' => 0, + 'query_builder_method' => null, + 'sleep' => 0 + )); + } + + /** + * {@inheritDoc} + */ + protected function disableLogging() + { + } + + /** + * {@inheritDoc} + */ + protected function enableLogging($logger) + { + } } diff --git a/Provider/AbstractProvider.php b/Provider/AbstractProvider.php index 82ea914..f05ab98 100644 --- a/Provider/AbstractProvider.php +++ b/Provider/AbstractProvider.php @@ -3,16 +3,17 @@ namespace FOS\ElasticaBundle\Provider; use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; /** - * AbstractProvider + * AbstractProvider. */ abstract class AbstractProvider implements ProviderInterface { /** - * @var ObjectPersisterInterface + * @var array */ - protected $objectPersister; + protected $baseOptions; /** * @var string @@ -20,12 +21,17 @@ abstract class AbstractProvider implements ProviderInterface protected $objectClass; /** - * @var array + * @var ObjectPersisterInterface */ - protected $options; + protected $objectPersister; /** - * @var Indexable + * @var OptionsResolver + */ + protected $resolver; + + /** + * @var IndexableInterface */ private $indexable; @@ -33,42 +39,136 @@ abstract class AbstractProvider implements ProviderInterface * Constructor. * * @param ObjectPersisterInterface $objectPersister - * @param IndexableInterface $indexable - * @param string $objectClass - * @param array $options + * @param IndexableInterface $indexable + * @param string $objectClass + * @param array $baseOptions */ public function __construct( ObjectPersisterInterface $objectPersister, IndexableInterface $indexable, $objectClass, - array $options = array() + array $baseOptions = array() ) { + $this->baseOptions = $baseOptions; $this->indexable = $indexable; $this->objectClass = $objectClass; $this->objectPersister = $objectPersister; + $this->resolver = new OptionsResolver(); + $this->configureOptions(); + } - $this->options = array_merge(array( + /** + * {@inheritDoc} + */ + public function populate(\Closure $loggerClosure = null, array $options = array()) + { + $options = $this->resolveOptions($options); + + $logger = !$options['debug_logging'] ? + $this->disableLogging() : + null; + + $this->doPopulate($options, $loggerClosure); + + if (null !== $logger) { + $this->enableLogging($logger); + } + } + + /** + * Disables logging and returns the logger that was previously set. + * + * @return mixed + */ + abstract protected function disableLogging(); + + /** + * Perform actual population. + * + * @param array $options + * @param \Closure $loggerClosure + */ + abstract protected function doPopulate($options, \Closure $loggerClosure = null); + + /** + * Reenables the logger with the previously returned logger from disableLogging();. + * + * @param mixed $logger + * + * @return mixed + */ + abstract protected function enableLogging($logger); + + /** + * Configures the option resolver. + */ + protected function configureOptions() + { + $this->resolver->setDefaults(array( 'batch_size' => 100, - ), $options); + 'skip_indexable_check' => false, + )); + $this->resolver->setAllowedTypes(array( + 'batch_size' => 'int' + )); + + $this->resolver->setRequired(array( + 'indexName', + 'typeName', + )); + } + + + /** + * Filters objects away if they are not indexable. + * + * @param array $options + * @param array $objects + * @return array + */ + protected function filterObjects(array $options, array $objects) + { + if ($options['skip_indexable_check']) { + return $objects; + } + + $index = $options['indexName']; + $type = $options['typeName']; + + $return = array(); + foreach ($objects as $object) { + if (!$this->indexable->isObjectIndexable($index, $type, $object)) { + continue; + } + + $return[] = $object; + } + + return $return; } /** * Checks if a given object should be indexed or not. * + * @deprecated To be removed in 4.0 + * * @param object $object + * * @return bool */ protected function isObjectIndexable($object) { return $this->indexable->isObjectIndexable( - $this->options['indexName'], - $this->options['typeName'], + $this->baseOptions['indexName'], + $this->baseOptions['typeName'], $object ); } /** - * Get string with RAM usage information (current and peak) + * Get string with RAM usage information (current and peak). + * + * @deprecated To be removed in 4.0 * * @return string */ @@ -79,4 +179,17 @@ abstract class AbstractProvider implements ProviderInterface return sprintf('(RAM : current=%uMo peak=%uMo)', $memory, $memoryMax); } + + /** + * Merges the base options provided by the class with options passed to the populate + * method and runs them through the resolver. + * + * @param array $options + * + * @return array + */ + protected function resolveOptions(array $options) + { + return $this->resolver->resolve(array_merge($this->baseOptions, $options)); + } } diff --git a/Provider/Indexable.php b/Provider/Indexable.php index 197aeb8..c26da5a 100644 --- a/Provider/Indexable.php +++ b/Provider/Indexable.php @@ -33,7 +33,7 @@ class Indexable implements IndexableInterface private $container; /** - * An instance of ExpressionLanguage + * An instance of ExpressionLanguage. * * @var ExpressionLanguage */ @@ -47,7 +47,7 @@ class Indexable implements IndexableInterface private $initialisedCallbacks = array(); /** - * PropertyAccessor instance + * PropertyAccessor instance. * * @var PropertyAccessorInterface */ @@ -55,6 +55,7 @@ class Indexable implements IndexableInterface /** * @param array $callbacks + * @param ContainerInterface $container */ public function __construct(array $callbacks, ContainerInterface $container) { @@ -68,7 +69,8 @@ class Indexable implements IndexableInterface * * @param string $indexName * @param string $typeName - * @param mixed $object + * @param mixed $object + * * @return bool */ public function isObjectIndexable($indexName, $typeName, $object) @@ -80,9 +82,9 @@ class Indexable implements IndexableInterface } if ($callback instanceof Expression) { - return $this->getExpressionLanguage()->evaluate($callback, array( + return (bool) $this->getExpressionLanguage()->evaluate($callback, array( 'object' => $object, - $this->getExpressionVar($object) => $object + $this->getExpressionVar($object) => $object, )); } @@ -96,12 +98,13 @@ class Indexable implements IndexableInterface * * @param string $type * @param object $object + * * @return mixed */ private function buildCallback($type, $object) { if (!array_key_exists($type, $this->callbacks)) { - return null; + return; } $callback = $this->callbacks[$type]; @@ -110,44 +113,54 @@ class Indexable implements IndexableInterface return $callback; } - if (is_array($callback)) { - list($class, $method) = $callback + array(null, null); - - if (is_object($class)) { - $class = get_class($class); - } - - if (strpos($class, '@') === 0) { - $service = $this->container->get(substr($class, 1)); - - return array($service, $method); - } - - if ($class && $method) { - throw new \InvalidArgumentException(sprintf('Callback for type "%s", "%s::%s()", is not callable.', $type, $class, $method)); - } + if (is_array($callback) && !is_object($callback[0])) { + return $this->processArrayToCallback($type, $callback); } - if (is_string($callback) && $expression = $this->getExpressionLanguage()) { - $callback = new Expression($callback); - - try { - $expression->compile($callback, array('object', $this->getExpressionVar($object))); - - return $callback; - } catch (SyntaxError $e) { - throw new \InvalidArgumentException(sprintf('Callback for type "%s" is an invalid expression', $type), $e->getCode(), $e); - } + if (is_string($callback)) { + return $this->buildExpressionCallback($type, $object, $callback); } throw new \InvalidArgumentException(sprintf('Callback for type "%s" is not a valid callback.', $type)); } + /** + * Processes a string expression into an Expression. + * + * @param string $type + * @param mixed $object + * @param string $callback + * + * @return Expression + */ + private function buildExpressionCallback($type, $object, $callback) + { + $expression = $this->getExpressionLanguage(); + if (!$expression) { + throw new \RuntimeException('Unable to process an expression without the ExpressionLanguage component.'); + } + + try { + $callback = new Expression($callback); + $expression->compile($callback, array( + 'object', $this->getExpressionVar($object) + )); + + return $callback; + } catch (SyntaxError $e) { + throw new \InvalidArgumentException(sprintf( + 'Callback for type "%s" is an invalid expression', + $type + ), $e->getCode(), $e); + } + } + /** * Retreives a cached callback, or creates a new callback if one is not found. * * @param string $type * @param object $object + * * @return mixed */ private function getCallback($type, $object) @@ -160,15 +173,13 @@ class Indexable implements IndexableInterface } /** - * @return bool|ExpressionLanguage + * Returns the ExpressionLanguage class if it is available. + * + * @return ExpressionLanguage|null */ private function getExpressionLanguage() { - if (null === $this->expressionLanguage) { - if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { - return false; - } - + if (null === $this->expressionLanguage && class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { $this->expressionLanguage = new ExpressionLanguage(); } @@ -176,13 +187,54 @@ class Indexable implements IndexableInterface } /** + * Returns the variable name to be used to access the object when using the ExpressionLanguage + * component to parse and evaluate an expression. + * * @param mixed $object + * * @return string */ private function getExpressionVar($object = null) { + if (!is_object($object)) { + return 'object'; + } + $ref = new \ReflectionClass($object); return strtolower($ref->getShortName()); } + + /** + * Processes an array into a callback. Replaces the first element with a service if + * it begins with an @. + * + * @param string $type + * @param array $callback + * @return array + */ + private function processArrayToCallback($type, array $callback) + { + list($class, $method) = $callback + array(null, '__invoke'); + + if (strpos($class, '@') === 0) { + $service = $this->container->get(substr($class, 1)); + $callback = array($service, $method); + + if (!is_callable($callback)) { + throw new \InvalidArgumentException(sprintf( + 'Method "%s" on service "%s" is not callable.', + $method, + substr($class, 1) + )); + } + + return $callback; + } + + throw new \InvalidArgumentException(sprintf( + 'Unable to parse callback array for type "%s"', + $type + )); + } } diff --git a/Provider/IndexableInterface.php b/Provider/IndexableInterface.php index 4871b58..0d9f047 100644 --- a/Provider/IndexableInterface.php +++ b/Provider/IndexableInterface.php @@ -18,7 +18,8 @@ interface IndexableInterface * * @param string $indexName * @param string $typeName - * @param mixed $object + * @param mixed $object + * * @return bool */ public function isObjectIndexable($indexName, $typeName, $object); diff --git a/Provider/ProviderInterface.php b/Provider/ProviderInterface.php index e8d7ea4..1ba977b 100644 --- a/Provider/ProviderInterface.php +++ b/Provider/ProviderInterface.php @@ -3,7 +3,7 @@ namespace FOS\ElasticaBundle\Provider; /** - * Insert application domain objects into elastica types + * Insert application domain objects into elastica types. * * @author Thibault Duplessis */ @@ -12,9 +12,15 @@ interface ProviderInterface /** * Persists all domain objects to ElasticSearch for this provider. * + * The closure can expect 2 or 3 arguments: + * * The step size + * * The total number of objects + * * A message to output in error conditions (not normally provided) + * * @param \Closure $loggerClosure * @param array $options + * * @return */ - function populate(\Closure $loggerClosure = null, array $options = array()); + public function populate(\Closure $loggerClosure = null, array $options = array()); } diff --git a/Provider/ProviderRegistry.php b/Provider/ProviderRegistry.php index 2142223..9fc9e3c 100644 --- a/Provider/ProviderRegistry.php +++ b/Provider/ProviderRegistry.php @@ -57,8 +57,10 @@ class ProviderRegistry implements ContainerAwareInterface * * Providers will be indexed by "type" strings in the returned array. * - * @param string $index - * @return array of ProviderInterface instances + * @param string $index + * + * @return ProviderInterface[] + * * @throws \InvalidArgumentException if no providers were registered for the index */ public function getIndexProviders($index) @@ -81,7 +83,9 @@ class ProviderRegistry implements ContainerAwareInterface * * @param string $index * @param string $type + * * @return ProviderInterface + * * @throws \InvalidArgumentException if no provider was registered for the index and type */ public function getProvider($index, $type) diff --git a/README.md b/README.md index 631951a..797d629 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,16 @@ Symfony2. Features include: > **Note** Propel support is limited and contributions fixing issues are welcome! [![Build Status](https://secure.travis-ci.org/FriendsOfSymfony/FOSElasticaBundle.png?branch=master)](http://travis-ci.org/FriendsOfSymfony/FOSElasticaBundle) [![Total Downloads](https://poser.pugx.org/FriendsOfSymfony/elastica-bundle/downloads.png)](https://packagist.org/packages/FriendsOfSymfony/elastica-bundle) [![Latest Stable Version](https://poser.pugx.org/FriendsOfSymfony/elastica-bundle/v/stable.png)](https://packagist.org/packages/FriendsOfSymfony/elastica-bundle) [![Latest Unstable Version](https://poser.pugx.org/friendsofsymfony/elastica-bundle/v/unstable.svg)](https://packagist.org/packages/friendsofsymfony/elastica-bundle) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/FriendsOfSymfony/FOSElasticaBundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/FriendsOfSymfony/FOSElasticaBundle/?branch=master) Documentation ------------- Documentation for FOSElasticaBundle is in `Resources/doc/index.md` -[Read the documentation for 3.0.x (master)](https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/master/Resources/doc/index.md) +[Read the documentation for 3.1.x](https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/master/Resources/doc/index.md) -[Read the documentation for 2.1.x](https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/2.1.x/README.md) +[Read the documentation for 3.0.x](https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/3.0.x/Resources/doc/index.md) Installation ------------ diff --git a/Repository.php b/Repository.php index 70b2a21..04a51c5 100644 --- a/Repository.php +++ b/Repository.php @@ -19,21 +19,47 @@ class Repository $this->finder = $finder; } + /** + * @param mixed $query + * @param integer $limit + * @param array $options + * + * @return array + */ public function find($query, $limit = null, $options = array()) { return $this->finder->find($query, $limit, $options); } + /** + * @param mixed $query + * @param integer $limit + * @param array $options + * + * @return mixed + */ public function findHybrid($query, $limit = null, $options = array()) { return $this->finder->findHybrid($query, $limit, $options); } + /** + * @param mixed $query + * @param array $options + * + * @return \Pagerfanta\Pagerfanta + */ public function findPaginated($query, $options = array()) { return $this->finder->findPaginated($query, $options); } + /** + * @param string $query + * @param array $options + * + * @return Paginator\PaginatorAdapterInterface + */ public function createPaginatorAdapter($query, $options = array()) { return $this->finder->createPaginatorAdapter($query, $options); diff --git a/Resources/config/index.xml b/Resources/config/index.xml index 11586ff..3ae2e50 100644 --- a/Resources/config/index.xml +++ b/Resources/config/index.xml @@ -41,6 +41,7 @@ + diff --git a/Resources/config/mongodb.xml b/Resources/config/mongodb.xml index 048d799..97ed16e 100644 --- a/Resources/config/mongodb.xml +++ b/Resources/config/mongodb.xml @@ -4,24 +4,35 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + FOS\ElasticaBundle\Doctrine\MongoDB\SliceFetcher + FOS\ElasticaBundle\Doctrine\MongoDB\Provider + FOS\ElasticaBundle\Doctrine\Listener + FOS\ElasticaBundle\Doctrine\MongoDB\ElasticaToModelTransformer + FOS\ElasticaBundle\Doctrine\RepositoryManager + + - + + + + - + + - + - - + null - + @@ -30,7 +41,7 @@ - + diff --git a/Resources/config/orm.xml b/Resources/config/orm.xml index ddc0e50..8147d51 100644 --- a/Resources/config/orm.xml +++ b/Resources/config/orm.xml @@ -4,24 +4,35 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + FOS\ElasticaBundle\Doctrine\ORM\SliceFetcher + FOS\ElasticaBundle\Doctrine\ORM\Provider + FOS\ElasticaBundle\Doctrine\Listener + FOS\ElasticaBundle\Doctrine\ORM\ElasticaToModelTransformer + FOS\ElasticaBundle\Doctrine\RepositoryManager + + - + + + + - + + - + - - + null - + @@ -30,7 +41,7 @@ - + diff --git a/Resources/config/transformer.xml b/Resources/config/transformer.xml index 4ce5062..0957152 100644 --- a/Resources/config/transformer.xml +++ b/Resources/config/transformer.xml @@ -12,7 +12,8 @@ - + + diff --git a/Resources/doc/cookbook/custom-properties.md b/Resources/doc/cookbook/custom-properties.md new file mode 100644 index 0000000..1d7687e --- /dev/null +++ b/Resources/doc/cookbook/custom-properties.md @@ -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', + ); + } +} +``` diff --git a/Resources/doc/cookbook/custom-repositories.md b/Resources/doc/cookbook/custom-repositories.md index 9eff5f7..866f72d 100644 --- a/Resources/doc/cookbook/custom-repositories.md +++ b/Resources/doc/cookbook/custom-repositories.md @@ -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 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 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 ----------------------------------------------- @@ -173,13 +201,18 @@ index enabled users. The callback option supports multiple approaches: * A method on the object itself provided as a string. `enabled` will call - `Object->enabled()` + `Object->enabled()`. Note that this does not support chaining methods with dot notation + like property paths. To achieve something similar use the ExpressionLanguage option + below. * An array of a service id and a method which will be called with the object as the first and only argument. `[ @my_custom_service, 'userIndexable' ]` will call the userIndexable method on a service defined as my_custom_service. * An array of a class and a static method to call on that class which will be called with the object as the only argument. `[ 'Acme\DemoBundle\IndexableChecker', 'isIndexable' ]` will call Acme\DemoBundle\IndexableChecker::isIndexable($object) +* A single element array with a service id can be used if the service has an __invoke + method. Such an invoke method must accept a single parameter for the object to be indexed. + `[ @my_custom_invokable_service ]` * If you have the ExpressionLanguage component installed, A valid ExpressionLanguage expression provided as a string. The object being indexed will be supplied as `object` in the expression. `object.isEnabled() or object.shouldBeIndexedAnyway()`. For more diff --git a/Resources/doc/usage.md b/Resources/doc/usage.md index 37514b3..be11dbf 100644 --- a/Resources/doc/usage.md +++ b/Resources/doc/usage.md @@ -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 diff --git a/Resources/public/images/elastica.png b/Resources/public/images/elastica.png deleted file mode 100644 index dbde014..0000000 Binary files a/Resources/public/images/elastica.png and /dev/null differ diff --git a/Resources/views/Collector/elastica.html.twig b/Resources/views/Collector/elastica.html.twig index e6d7072..82a3dcf 100644 --- a/Resources/views/Collector/elastica.html.twig +++ b/Resources/views/Collector/elastica.html.twig @@ -23,7 +23,7 @@ {% block menu %} - + Elastica {{ collector.querycount }} diff --git a/Serializer/Callback.php b/Serializer/Callback.php index 38d93dc..61da997 100644 --- a/Serializer/Callback.php +++ b/Serializer/Callback.php @@ -8,7 +8,7 @@ use JMS\Serializer\SerializerInterface; class Callback { protected $serializer; - protected $groups; + protected $groups = array(); protected $version; public function setSerializer($serializer) @@ -23,10 +23,8 @@ class Callback { $this->groups = $groups; - if ($this->groups) { - if (!$this->serializer instanceof SerializerInterface) { - throw new \RuntimeException('Setting serialization groups requires using "JMS\Serializer\Serializer".'); - } + if (!empty($this->groups) && !$this->serializer instanceof SerializerInterface) { + throw new \RuntimeException('Setting serialization groups requires using "JMS\Serializer\Serializer".'); } } @@ -34,10 +32,8 @@ class Callback { $this->version = $version; - if ($this->version) { - if (!$this->serializer instanceof SerializerInterface) { - throw new \RuntimeException('Setting serialization version requires using "JMS\Serializer\Serializer".'); - } + if ($this->version && !$this->serializer instanceof SerializerInterface) { + throw new \RuntimeException('Setting serialization version requires using "JMS\Serializer\Serializer".'); } } @@ -45,7 +41,7 @@ class Callback { $context = $this->serializer instanceof SerializerInterface ? SerializationContext::create()->enableMaxDepthChecks() : array(); - if ($this->groups) { + if (!empty($this->groups)) { $context->setGroups($this->groups); } diff --git a/Subscriber/PaginateElasticaQuerySubscriber.php b/Subscriber/PaginateElasticaQuerySubscriber.php index 0b7cfd6..63f6cd0 100644 --- a/Subscriber/PaginateElasticaQuerySubscriber.php +++ b/Subscriber/PaginateElasticaQuerySubscriber.php @@ -32,13 +32,17 @@ class PaginateElasticaQuerySubscriber implements EventSubscriberInterface if (null != $facets) { $event->setCustomPaginationParameter('facets', $facets); } + $aggregations = $results->getAggregations(); + if (null != $aggregations) { + $event->setCustomPaginationParameter('aggregations', $aggregations); + } $event->stopPropagation(); } } /** - * Adds knp paging sort to query + * Adds knp paging sort to query. * * @param ItemsEvent $event */ @@ -70,7 +74,7 @@ class PaginateElasticaQuerySubscriber implements EventSubscriberInterface public static function getSubscribedEvents() { return array( - 'knp_pager.items' => array('items', 1) + 'knp_pager.items' => array('items', 1), ); } -} \ No newline at end of file +} diff --git a/Tests/Command/ResetCommandTest.php b/Tests/Command/ResetCommandTest.php index b6548aa..d63b380 100644 --- a/Tests/Command/ResetCommandTest.php +++ b/Tests/Command/ResetCommandTest.php @@ -2,7 +2,6 @@ namespace FOS\ElasticaBundle\Tests\Command; - use FOS\ElasticaBundle\Command\ResetCommand; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; @@ -10,8 +9,8 @@ use Symfony\Component\DependencyInjection\Container; class ResetCommandTest extends \PHPUnit_Framework_TestCase { + private $command; private $resetter; - private $indexManager; public function setup() @@ -88,4 +87,4 @@ class ResetCommandTest extends \PHPUnit_Framework_TestCase new NullOutput() ); } -} \ No newline at end of file +} diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 7165052..062db5c 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -6,7 +6,7 @@ use FOS\ElasticaBundle\DependencyInjection\Configuration; use Symfony\Component\Config\Definition\Processor; /** - * ConfigurationTest + * ConfigurationTest. */ class ConfigurationTest extends \PHPUnit_Framework_TestCase { @@ -34,7 +34,7 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase $this->assertSame(array( 'clients' => array(), 'indexes' => array(), - 'default_manager' => 'orm' + 'default_manager' => 'orm', ), $configuration); } @@ -50,18 +50,18 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase array( 'url' => 'http://es1:9200', 'headers' => array( - 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' - ) + 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', + ), ), array( 'url' => 'http://es2:9200', 'headers' => array( - 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' - ) + 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', + ), ), - ) - ) - ) + ), + ), + ), )); $this->assertCount(2, $configuration['clients']); @@ -91,9 +91,9 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase ), 'logging_custom' => array( 'url' => 'http://localhost:9200', - 'logger' => 'custom.service' + 'logger' => 'custom.service', ), - ) + ), )); $this->assertCount(4, $configuration['clients']); @@ -131,8 +131,8 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase ), 'serializer' => array( 'groups' => array('Search'), - 'version' => 1 - ) + 'version' => 1, + ), ), 'types' => array( 'test' => array( @@ -144,20 +144,20 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase 'persistence' => array( 'listener' => array( 'logger' => true, - ) - ) + ), + ), ), 'test2' => array( 'mappings' => array( 'title' => null, 'children' => array( 'type' => 'nested', - ) - ) - ) - ) - ) - ) + ), + ), + ), + ), + ), + ), )); } @@ -169,7 +169,7 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase 'host' => 'localhost', 'port' => 9200, ), - ) + ), )); $this->assertTrue(empty($configuration['clients']['default']['connections'][0]['url'])); @@ -189,16 +189,34 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase 'title' => array(), 'published' => array('type' => 'datetime'), 'body' => null, - ) - ) - ) - ) - ) + ), + ), + ), + ), + ), )); $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( @@ -225,23 +243,23 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase 'type' => 'nested', 'properties' => array( 'nested_field1' => array( - 'type' => 'integer' + 'type' => 'integer', ), 'nested_field2' => array( 'type' => 'object', 'properties' => array( 'id' => array( - 'type' => 'integer' - ) - ) - ) - ) - ) - ) - ) - ) - ) - ) + 'type' => 'integer', + ), + ), + ), + ), + ), + ), + ), + ), + ), + ), )); } } diff --git a/Tests/DependencyInjection/FOSElasticaExtensionTest.php b/Tests/DependencyInjection/FOSElasticaExtensionTest.php new file mode 100644 index 0000000..1bef2b6 --- /dev/null +++ b/Tests/DependencyInjection/FOSElasticaExtensionTest.php @@ -0,0 +1,32 @@ +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']); + } +} diff --git a/Tests/DependencyInjection/fixtures/config.yml b/Tests/DependencyInjection/fixtures/config.yml new file mode 100644 index 0000000..5528d18 --- /dev/null +++ b/Tests/DependencyInjection/fixtures/config.yml @@ -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" } diff --git a/Tests/Doctrine/AbstractElasticaToModelTransformerTest.php b/Tests/Doctrine/AbstractElasticaToModelTransformerTest.php index 325171b..1185e74 100644 --- a/Tests/Doctrine/AbstractElasticaToModelTransformerTest.php +++ b/Tests/Doctrine/AbstractElasticaToModelTransformerTest.php @@ -19,7 +19,7 @@ class AbstractElasticaToModelTransformerTest extends \PHPUnit_Framework_TestCase protected $objectClass = 'stdClass'; /** - * Tests if ignore_missing option is properly handled in transformHybrid() method + * Tests if ignore_missing option is properly handled in transformHybrid() method. */ public function testIgnoreMissingOptionDuringTransformHybrid() { @@ -55,10 +55,6 @@ class AbstractElasticaToModelTransformerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!interface_exists('Doctrine\Common\Persistence\ManagerRegistry')) { - $this->markTestSkipped('Doctrine Common is not present'); - } - $this->registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry') ->disableOriginalConstructor() ->getMock(); diff --git a/Tests/Doctrine/AbstractListenerTest.php b/Tests/Doctrine/AbstractListenerTest.php index 1f238d6..dcaf7d6 100644 --- a/Tests/Doctrine/AbstractListenerTest.php +++ b/Tests/Doctrine/AbstractListenerTest.php @@ -3,7 +3,7 @@ namespace FOS\ElasticaBundle\Tests\Doctrine; /** - * See concrete MongoDB/ORM instances of this abstract test + * See concrete MongoDB/ORM instances of this abstract test. * * @author Richard Miller */ @@ -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)); @@ -173,8 +173,14 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase abstract protected function getListenerClass(); + /** + * @return string + */ abstract protected function getObjectManagerClass(); + /** + * @return string + */ abstract protected function getClassMetadataClass(); private function createLifecycleEventArgs() @@ -205,6 +211,11 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase ->getMock(); } + /** + * @param Listener\Entity $object + * @param string $indexName + * @param string $typeName + */ private function getMockPersister($object, $indexName, $typeName) { $mock = $this->getMockBuilder('FOS\ElasticaBundle\Persister\ObjectPersister') @@ -235,6 +246,12 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase return $mock; } + /** + * @param string $indexName + * @param string $typeName + * @param Listener\Entity $object + * @param boolean $return + */ private function getMockIndexable($indexName, $typeName, $object, $return = null) { $mock = $this->getMock('FOS\ElasticaBundle\Provider\IndexableInterface'); @@ -255,7 +272,11 @@ namespace FOS\ElasticaBundle\Tests\Doctrine\Listener; class Entity { private $id; + public $identifier; + /** + * @param integer $id + */ public function __construct($id) { $this->id = $id; @@ -266,4 +287,3 @@ class Entity return $this->id; } } - diff --git a/Tests/Doctrine/AbstractProviderTest.php b/Tests/Doctrine/AbstractProviderTest.php index 99ed2de..aa28a4c 100644 --- a/Tests/Doctrine/AbstractProviderTest.php +++ b/Tests/Doctrine/AbstractProviderTest.php @@ -13,13 +13,10 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase private $options; private $managerRegistry; private $indexable; + private $sliceFetcher; public function setUp() { - if (!interface_exists('Doctrine\Common\Persistence\ManagerRegistry')) { - $this->markTestSkipped('Doctrine Common is not available.'); - } - $this->objectClass = 'objectClass'; $this->options = array('debug_logging' => true, 'indexName' => 'index', 'typeName' => 'type'); @@ -32,6 +29,8 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->method('getManagerForClass') ->with($this->objectClass) ->will($this->returnValue($this->objectManager)); + + $this->sliceFetcher = $this->getMockSliceFetcher(); } /** @@ -45,6 +44,53 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase $queryBuilder = new \stdClass(); + $provider->expects($this->once()) + ->method('createQueryBuilder') + ->will($this->returnValue($queryBuilder)); + + $provider->expects($this->once()) + ->method('countObjects') + ->with($queryBuilder) + ->will($this->returnValue($nbObjects)); + + $this->indexable->expects($this->any()) + ->method('isObjectIndexable') + ->with('index', 'type', $this->anything()) + ->will($this->returnValue(true)); + + $previousSlice = array(); + + foreach ($objectsByIteration as $i => $objects) { + $offset = $objects[0] - 1; + + $this->sliceFetcher->expects($this->at($i)) + ->method('fetch') + ->with($queryBuilder, $batchSize, $offset, $previousSlice, array('id')) + ->will($this->returnValue($objects)); + + $this->objectManager->expects($this->at($i)) + ->method('clear'); + + $previousSlice = $objects; + } + + $this->objectPersister->expects($this->exactly(count($objectsByIteration))) + ->method('insertMany'); + + $provider->populate(); + } + + /** + * @dataProvider providePopulateIterations + */ + public function testPopulateIterationsWithoutSliceFetcher($nbObjects, $objectsByIteration, $batchSize) + { + $this->options['batch_size'] = $batchSize; + + $provider = $this->getMockAbstractProvider(false); + + $queryBuilder = new \stdClass(); + $provider->expects($this->once()) ->method('createQueryBuilder') ->will($this->returnValue($queryBuilder)); @@ -84,7 +130,7 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase return array( array( 100, - array(range(1,100)), + array(range(1, 100)), 100, ), array( @@ -107,8 +153,8 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->method('countObjects') ->will($this->returnValue($nbObjects)); - $provider->expects($this->any()) - ->method('fetchSlice') + $this->sliceFetcher->expects($this->any()) + ->method('fetch') ->will($this->returnValue($objects)); $this->indexable->expects($this->any()) @@ -122,6 +168,32 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase $provider->populate(); } + public function testPopulateShouldClearObjectManagerForFilteredBatch() + { + $nbObjects = 1; + $objects = array(1); + + $provider = $this->getMockAbstractProvider(true); + + $provider->expects($this->any()) + ->method('countObjects') + ->will($this->returnValue($nbObjects)); + + $this->sliceFetcher->expects($this->any()) + ->method('fetch') + ->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; @@ -133,8 +205,8 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->method('countObjects') ->will($this->returnValue($nbObjects)); - $provider->expects($this->any()) - ->method('fetchSlice') + $this->sliceFetcher->expects($this->any()) + ->method('fetch') ->will($this->returnValue($objects)); $this->indexable->expects($this->any()) @@ -165,8 +237,8 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->method('countObjects') ->will($this->returnValue($nbObjects)); - $provider->expects($this->any()) - ->method('fetchSlice') + $this->sliceFetcher->expects($this->any()) + ->method('fetch') ->will($this->returnValue($objects)); $this->indexable->expects($this->any()) @@ -180,7 +252,7 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase $this->setExpectedException('Elastica\Exception\Bulk\ResponseException'); - $provider->populate(null, array('ignore-errors' => false)); + $provider->populate(null, array('ignore_errors' => false)); } public function testPopulateRunsIndexCallable() @@ -192,8 +264,9 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase $provider->expects($this->any()) ->method('countObjects') ->will($this->returnValue($nbObjects)); - $provider->expects($this->any()) - ->method('fetchSlice') + + $this->sliceFetcher->expects($this->any()) + ->method('fetch') ->will($this->returnValue($objects)); $this->indexable->expects($this->at(0)) @@ -205,18 +278,19 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->with('index', 'type', 2) ->will($this->returnValue(true)); - $this->objectPersister->expects($this->once()) ->method('insertMany') - ->with(array(1 => 2)); + ->with(array(2)); $provider->populate(); } /** + * @param boolean $setSliceFetcher Whether or not to set the slice fetcher. + * * @return \FOS\ElasticaBundle\Doctrine\AbstractProvider|\PHPUnit_Framework_MockObject_MockObject */ - private function getMockAbstractProvider() + private function getMockAbstractProvider($setSliceFetcher = true) { return $this->getMockForAbstractClass('FOS\ElasticaBundle\Doctrine\AbstractProvider', array( $this->objectPersister, @@ -224,6 +298,7 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase $this->objectClass, $this->options, $this->managerRegistry, + $setSliceFetcher ? $this->sliceFetcher : null )); } @@ -233,7 +308,7 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase private function getMockBulkResponseException() { return $this->getMock('Elastica\Exception\Bulk\ResponseException', null, array( - new ResponseSet(new Response(array()), array()) + new ResponseSet(new Response(array()), array()), )); } @@ -250,7 +325,17 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase */ private function getMockObjectManager() { - return $this->getMock(__NAMESPACE__ . '\ObjectManager'); + $mock = $this->getMock(__NAMESPACE__.'\ObjectManager'); + + $mock->expects($this->any()) + ->method('getClassMetadata') + ->will($this->returnSelf()); + + $mock->expects($this->any()) + ->method('getIdentifierFieldNames') + ->will($this->returnValue(array('id'))); + + return $mock; } /** @@ -268,6 +353,14 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase { return $this->getMock('FOS\ElasticaBundle\Provider\IndexableInterface'); } + + /** + * @return \FOS\ElasticaBundle\Doctrine\SliceFetcherInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private function getMockSliceFetcher() + { + return $this->getMock('FOS\ElasticaBundle\Doctrine\SliceFetcherInterface'); + } } /** @@ -276,5 +369,7 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase */ interface ObjectManager { - function clear(); + public function clear(); + public function getClassMetadata(); + public function getIdentifierFieldNames(); } diff --git a/Tests/Doctrine/ORM/ElasticaToModelTransformerTest.php b/Tests/Doctrine/ORM/ElasticaToModelTransformerTest.php index 14f3ffb..607aeef 100644 --- a/Tests/Doctrine/ORM/ElasticaToModelTransformerTest.php +++ b/Tests/Doctrine/ORM/ElasticaToModelTransformerTest.php @@ -82,13 +82,6 @@ class ElasticaToModelTransformerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!interface_exists('Doctrine\Common\Persistence\ManagerRegistry')) { - $this->markTestSkipped('Doctrine Common is not present'); - } - if (!class_exists('Doctrine\ORM\EntityManager')) { - $this->markTestSkipped('Doctrine Common is not present'); - } - $this->registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry') ->disableOriginalConstructor() ->getMock(); @@ -109,7 +102,7 @@ class ElasticaToModelTransformerTest extends \PHPUnit_Framework_TestCase 'findAll', 'findBy', 'findOneBy', - 'getClassName' + 'getClassName', )); $this->manager->expects($this->any()) diff --git a/Tests/Doctrine/ORM/ListenerTest.php b/Tests/Doctrine/ORM/ListenerTest.php index 12a89b2..36cacc6 100644 --- a/Tests/Doctrine/ORM/ListenerTest.php +++ b/Tests/Doctrine/ORM/ListenerTest.php @@ -6,13 +6,6 @@ use FOS\ElasticaBundle\Tests\Doctrine\ListenerTest as BaseListenerTest; class ListenerTest extends BaseListenerTest { - public function setUp() - { - if (!class_exists('Doctrine\ORM\EntityManager')) { - $this->markTestSkipped('Doctrine ORM is not available.'); - } - } - protected function getClassMetadataClass() { return 'Doctrine\ORM\Mapping\ClassMetadata'; diff --git a/Tests/Doctrine/RepositoryManagerTest.php b/Tests/Doctrine/RepositoryManagerTest.php index ce7b14b..39f9c34 100644 --- a/Tests/Doctrine/RepositoryManagerTest.php +++ b/Tests/Doctrine/RepositoryManagerTest.php @@ -4,22 +4,19 @@ namespace FOS\ElasticaBundle\Tests\Doctrine; use FOS\ElasticaBundle\Doctrine\RepositoryManager; -class CustomRepository{} +class CustomRepository +{ +} -class Entity{} +class Entity +{ +} /** * @author Richard Miller */ class RepositoryManagerTest extends \PHPUnit_Framework_TestCase { - public function setUp() - { - if (!interface_exists('Doctrine\Common\Persistence\ManagerRegistry')) { - $this->markTestSkipped('Doctrine Common is not available.'); - } - } - public function testThatGetRepositoryReturnsDefaultRepository() { /** @var $finderMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ diff --git a/Tests/Elastica/ClientTest.php b/Tests/Elastica/ClientTest.php index 43ac7a2..158b553 100644 --- a/Tests/Elastica/ClientTest.php +++ b/Tests/Elastica/ClientTest.php @@ -5,11 +5,11 @@ namespace FOS\ElasticaBundle\Tests\Client; use Elastica\Request; use Elastica\Transport\Null as NullTransport; -class LoggingClientTest extends \PHPUnit_Framework_TestCase +class ClientTest extends \PHPUnit_Framework_TestCase { public function testRequestsAreLogged() { - $transport = new NullTransport; + $transport = new NullTransport(); $connection = $this->getMock('Elastica\Connection'); $connection->expects($this->any())->method('getTransportObject')->will($this->returnValue($transport)); diff --git a/Tests/FOSElasticaBundleTest.php b/Tests/FOSElasticaBundleTest.php index 3828e8b..c9513db 100644 --- a/Tests/FOSElasticaBundleTest.php +++ b/Tests/FOSElasticaBundleTest.php @@ -3,7 +3,6 @@ namespace FOS\ElasticaBundle\Tests\Resetter; use FOS\ElasticaBundle\FOSElasticaBundle; -use Symfony\Component\DependencyInjection\Compiler\PassConfig; class FOSElasticaBundleTest extends \PHPUnit_Framework_TestCase { @@ -17,7 +16,6 @@ class FOSElasticaBundleTest extends \PHPUnit_Framework_TestCase ->method('addCompilerPass') ->with($this->isInstanceOf('Symfony\\Component\\DependencyInjection\\Compiler\\CompilerPassInterface')); - $bundle = new FOSElasticaBundle(); $bundle->build($container); } diff --git a/Tests/Functional/ClientTest.php b/Tests/Functional/ClientTest.php new file mode 100644 index 0000000..8a6357a --- /dev/null +++ b/Tests/Functional/ClientTest.php @@ -0,0 +1,48 @@ + + * + * 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'); + } +} diff --git a/Tests/Functional/ConfigurationManagerTest.php b/Tests/Functional/ConfigurationManagerTest.php index 7ef02c5..a6028b7 100644 --- a/Tests/Functional/ConfigurationManagerTest.php +++ b/Tests/Functional/ConfigurationManagerTest.php @@ -47,6 +47,7 @@ class ConfigurationManagerTest extends WebTestCase /** * @param Client $client + * * @return \FOS\ElasticaBundle\Configuration\ConfigManager */ private function getManager(Client $client) diff --git a/Tests/Functional/IndexableCallbackTest.php b/Tests/Functional/IndexableCallbackTest.php index 41ed402..3f84286 100644 --- a/Tests/Functional/IndexableCallbackTest.php +++ b/Tests/Functional/IndexableCallbackTest.php @@ -17,7 +17,7 @@ namespace FOS\ElasticaBundle\Tests\Functional; class IndexableCallbackTest extends WebTestCase { /** - * 2 reasons for this test: + * 2 reasons for this test:. * * 1) To test that the configuration rename from is_indexable_callback under the listener * key is respected, and diff --git a/Tests/Functional/MappingToElasticaTest.php b/Tests/Functional/MappingToElasticaTest.php index f42df61..6f93b7e 100644 --- a/Tests/Functional/MappingToElasticaTest.php +++ b/Tests/Functional/MappingToElasticaTest.php @@ -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']); @@ -96,6 +103,7 @@ class MappingToElasticaTest extends WebTestCase /** * @param Client $client + * * @return \FOS\ElasticaBundle\Resetter $resetter */ private function getResetter(Client $client) @@ -105,11 +113,13 @@ class MappingToElasticaTest extends WebTestCase /** * @param Client $client + * @param string $type + * * @return \Elastica\Type */ private function getType(Client $client, $type = 'type') { - return $client->getContainer()->get('fos_elastica.index.index.' . $type); + return $client->getContainer()->get('fos_elastica.index.index.'.$type); } protected function setUp() diff --git a/Tests/Functional/PropertyPathTest.php b/Tests/Functional/PropertyPathTest.php new file mode 100644 index 0000000..860cb86 --- /dev/null +++ b/Tests/Functional/PropertyPathTest.php @@ -0,0 +1,54 @@ + + * + * 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'); + } +} diff --git a/Tests/Functional/TypeObj.php b/Tests/Functional/TypeObj.php index 46e5968..39e9fe9 100644 --- a/Tests/Functional/TypeObj.php +++ b/Tests/Functional/TypeObj.php @@ -13,8 +13,10 @@ namespace FOS\ElasticaBundle\Tests\Functional; class TypeObj { + public $id = 5; public $coll; public $field1; + public $field2; public function isIndexable() { diff --git a/Tests/Functional/app/AppKernel.php b/Tests/Functional/app/AppKernel.php index f47a5b3..d75910a 100644 --- a/Tests/Functional/app/AppKernel.php +++ b/Tests/Functional/app/AppKernel.php @@ -115,4 +115,4 @@ class AppKernel extends Kernel return $parameters; } -} \ No newline at end of file +} diff --git a/Tests/Functional/app/Basic/config.yml b/Tests/Functional/app/Basic/config.yml index 3c3d369..8ed88eb 100644 --- a/Tests/Functional/app/Basic/config.yml +++ b/Tests/Functional/app/Basic/config.yml @@ -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: ~ diff --git a/Tests/Functional/app/ORM/IndexableService.php b/Tests/Functional/app/ORM/IndexableService.php index 018451e..8f17bd0 100644 --- a/Tests/Functional/app/ORM/IndexableService.php +++ b/Tests/Functional/app/ORM/IndexableService.php @@ -13,12 +13,12 @@ namespace FOS\ElasticaBundle\Tests\Functional\app\ORM; class IndexableService { - public function isIndexable($object) + public function isIndexable() { return true; } - public static function isntIndexable($object) + public static function isntIndexable() { return false; } diff --git a/Tests/Functional/app/ORM/config.yml b/Tests/Functional/app/ORM/config.yml index 02d7a92..d2ff931 100644 --- a/Tests/Functional/app/ORM/config.yml +++ b/Tests/Functional/app/ORM/config.yml @@ -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: diff --git a/Tests/Functional/app/Serializer/config.yml b/Tests/Functional/app/Serializer/config.yml index 9bea4ba..98e59e8 100644 --- a/Tests/Functional/app/Serializer/config.yml +++ b/Tests/Functional/app/Serializer/config.yml @@ -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: @@ -47,4 +47,3 @@ fos_elastica: serializer: groups: ['search', 'Default'] version: 1.1 - diff --git a/Tests/Index/AliasProcessorTest.php b/Tests/Index/AliasProcessorTest.php new file mode 100644 index 0000000..f1592b2 --- /dev/null +++ b/Tests/Index/AliasProcessorTest.php @@ -0,0 +1,223 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Tests\Index; + +use Elastica\Exception\ResponseException; +use Elastica\Request; +use Elastica\Response; +use FOS\ElasticaBundle\Configuration\IndexConfig; +use FOS\ElasticaBundle\Index\AliasProcessor; + +class AliasProcessorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var AliasProcessor + */ + private $processor; + + /** + * @dataProvider getSetRootNameData + * @param string $name + * @param array $configArray + * @param string $resultStartsWith + */ + public function testSetRootName($name, $configArray, $resultStartsWith) + { + $indexConfig = new IndexConfig($name, array(), $configArray); + $index = $this->getMockBuilder('FOS\\ElasticaBundle\\Elastica\\Index') + ->disableOriginalConstructor() + ->getMock(); + $index->expects($this->once()) + ->method('overrideName') + ->with($this->stringStartsWith($resultStartsWith)); + + $this->processor->setRootName($indexConfig, $index); + } + + public function testSwitchAliasNoAliasSet() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array())); + $client->expects($this->at(1)) + ->method('request') + ->with('_aliases', 'POST', array('actions' => array( + array('add' => array('index' => 'unique_name', 'alias' => 'name')) + ))); + + $this->processor->switchIndexAlias($indexConfig, $index, false); + } + + public function testSwitchAliasExistingAliasSet() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array( + 'old_unique_name' => array('aliases' => array('name')) + ))); + $client->expects($this->at(1)) + ->method('request') + ->with('_aliases', 'POST', array('actions' => array( + array('remove' => array('index' => 'old_unique_name', 'alias' => 'name')), + array('add' => array('index' => 'unique_name', 'alias' => 'name')) + ))); + + $this->processor->switchIndexAlias($indexConfig, $index, false); + } + + /** + * @expectedException \RuntimeException + */ + public function testSwitchAliasThrowsWhenMoreThanOneExists() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array( + 'old_unique_name' => array('aliases' => array('name')), + 'another_old_unique_name' => array('aliases' => array('name')) + ))); + + $this->processor->switchIndexAlias($indexConfig, $index, false); + } + + /** + * @expectedException \FOS\ElasticaBundle\Exception\AliasIsIndexException + */ + public function testSwitchAliasThrowsWhenAliasIsAnIndex() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array( + 'name' => array(), + ))); + + $this->processor->switchIndexAlias($indexConfig, $index, false); + } + + public function testSwitchAliasDeletesIndexCollisionIfForced() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array( + 'name' => array(), + ))); + $client->expects($this->at(1)) + ->method('request') + ->with('name', 'DELETE'); + + $this->processor->switchIndexAlias($indexConfig, $index, true); + } + + public function testSwitchAliasDeletesOldIndex() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array( + 'old_unique_name' => array('aliases' => array('name')), + ))); + $client->expects($this->at(1)) + ->method('request') + ->with('_aliases', 'POST', array('actions' => array( + array('remove' => array('index' => 'old_unique_name', 'alias' => 'name')), + array('add' => array('index' => 'unique_name', 'alias' => 'name')) + ))); + $client->expects($this->at(2)) + ->method('request') + ->with('old_unique_name', 'DELETE'); + + $this->processor->switchIndexAlias($indexConfig, $index, true); + } + + public function testSwitchAliasCleansUpOnRenameFailure() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array( + 'old_unique_name' => array('aliases' => array('name')), + ))); + $client->expects($this->at(1)) + ->method('request') + ->with('_aliases', 'POST', array('actions' => array( + array('remove' => array('index' => 'old_unique_name', 'alias' => 'name')), + array('add' => array('index' => 'unique_name', 'alias' => 'name')) + ))) + ->will($this->throwException(new ResponseException(new Request(''), new Response('')))); + $client->expects($this->at(2)) + ->method('request') + ->with('unique_name', 'DELETE'); + // Not an annotation: we do not want a RuntimeException until now. + $this->setExpectedException('RuntimeException'); + + $this->processor->switchIndexAlias($indexConfig, $index, true); + } + + public function getSetRootNameData() + { + return array( + array('name', array(), 'name_'), + array('name', array('elasticSearchName' => 'notname'), 'notname_') + ); + } + + protected function setUp() + { + $this->processor = new AliasProcessor(); + } + + private function getMockedIndex($name) + { + $index = $this->getMockBuilder('FOS\\ElasticaBundle\\Elastica\\Index') + ->disableOriginalConstructor() + ->getMock(); + + $client = $this->getMockBuilder('Elastica\\Client') + ->disableOriginalConstructor() + ->getMock(); + $index->expects($this->any()) + ->method('getClient') + ->willReturn($client); + + $index->expects($this->any()) + ->method('getName') + ->willReturn($name); + + return array($index, $client); + } +} diff --git a/Tests/Index/IndexManagerTest.php b/Tests/Index/IndexManagerTest.php index 98e4d8a..78a3d28 100644 --- a/Tests/Index/IndexManagerTest.php +++ b/Tests/Index/IndexManagerTest.php @@ -13,7 +13,6 @@ class IndexManagerTest extends \PHPUnit_Framework_TestCase */ private $indexManager; - public function setUp() { foreach (array('index1', 'index2', 'index3') as $indexName) { diff --git a/Tests/Index/ResetterTest.php b/Tests/Index/ResetterTest.php index 28f0a68..9b4cd05 100644 --- a/Tests/Index/ResetterTest.php +++ b/Tests/Index/ResetterTest.php @@ -5,8 +5,14 @@ namespace FOS\ElasticaBundle\Tests\Index; use Elastica\Exception\ResponseException; use Elastica\Request; use Elastica\Response; +use Elastica\Type; use Elastica\Type\Mapping; use FOS\ElasticaBundle\Configuration\IndexConfig; +use FOS\ElasticaBundle\Configuration\TypeConfig; +use FOS\ElasticaBundle\Elastica\Index; +use FOS\ElasticaBundle\Event\IndexResetEvent; +use FOS\ElasticaBundle\Event\TypeResetEvent; +use FOS\ElasticaBundle\Index\AliasProcessor; use FOS\ElasticaBundle\Index\Resetter; class ResetterTest extends \PHPUnit_Framework_TestCase @@ -16,227 +22,253 @@ class ResetterTest extends \PHPUnit_Framework_TestCase */ private $resetter; - private $configManager; - private $indexManager; private $aliasProcessor; + private $configManager; + private $dispatcher; + private $elasticaClient; + private $indexManager; private $mappingBuilder; - public function setUp() + public function testResetAllIndexes() { - $this->markTestIncomplete('To be rewritten'); + $indexName = 'index1'; + $indexConfig = new IndexConfig($indexName, array(), array()); + $this->mockIndex($indexName, $indexConfig); + + $this->configManager->expects($this->once()) + ->method('getIndexNames') + ->will($this->returnValue(array($indexName))); + + $this->dispatcherExpects(array( + array(IndexResetEvent::PRE_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')), + array(IndexResetEvent::POST_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')) + )); + + $this->elasticaClient->expects($this->exactly(2)) + ->method('request') + ->withConsecutive( + array('index1/', 'DELETE'), + array('index1/', 'PUT', array(), array()) + ); + + $this->resetter->resetAllIndexes(); + } + + public function testResetIndex() + { + $indexConfig = new IndexConfig('index1', array(), array()); + $this->mockIndex('index1', $indexConfig); + + $this->dispatcherExpects(array( + array(IndexResetEvent::PRE_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')), + array(IndexResetEvent::POST_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')) + )); + + $this->elasticaClient->expects($this->exactly(2)) + ->method('request') + ->withConsecutive( + array('index1/', 'DELETE'), + array('index1/', 'PUT', array(), array()) + ); + + $this->resetter->resetIndex('index1'); + } + + public function testResetIndexWithDifferentName() + { + $indexConfig = new IndexConfig('index1', array(), array( + 'elasticSearchName' => 'notIndex1' + )); + $this->mockIndex('index1', $indexConfig); + $this->dispatcherExpects(array( + array(IndexResetEvent::PRE_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')), + array(IndexResetEvent::POST_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')) + )); + + $this->elasticaClient->expects($this->exactly(2)) + ->method('request') + ->withConsecutive( + array('index1/', 'DELETE'), + array('index1/', 'PUT', array(), array()) + ); + + $this->resetter->resetIndex('index1'); + } + + public function testResetIndexWithDifferentNameAndAlias() + { + $indexConfig = new IndexConfig('index1', array(), array( + 'elasticSearchName' => 'notIndex1', + 'useAlias' => true + )); + $index = $this->mockIndex('index1', $indexConfig); + $this->dispatcherExpects(array( + array(IndexResetEvent::PRE_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')), + array(IndexResetEvent::POST_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')) + )); + + $this->aliasProcessor->expects($this->once()) + ->method('switchIndexAlias') + ->with($indexConfig, $index, false); + + $this->elasticaClient->expects($this->exactly(2)) + ->method('request') + ->withConsecutive( + array('index1/', 'DELETE'), + array('index1/', 'PUT', array(), array()) + ); + + $this->resetter->resetIndex('index1'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testFailureWhenMissingIndexDoesntDispatch() + { + $this->configManager->expects($this->once()) + ->method('getIndexConfiguration') + ->with('nonExistant') + ->will($this->throwException(new \InvalidArgumentException)); + + $this->indexManager->expects($this->never()) + ->method('getIndex'); + + $this->resetter->resetIndex('nonExistant'); + } + + public function testResetType() + { + $typeConfig = new TypeConfig('type', array(), array()); + $this->mockType('type', 'index', $typeConfig); + + $this->dispatcherExpects(array( + array(TypeResetEvent::PRE_TYPE_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\TypeResetEvent')), + array(TypeResetEvent::POST_TYPE_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\TypeResetEvent')) + )); + + $this->elasticaClient->expects($this->exactly(2)) + ->method('request') + ->withConsecutive( + array('index/type/', 'DELETE'), + array('index/type/_mapping', 'PUT', array('type' => array()), array()) + ); + + $this->resetter->resetIndexType('index', 'type'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testNonExistantResetType() + { + $this->configManager->expects($this->once()) + ->method('getTypeConfiguration') + ->with('index', 'nonExistant') + ->will($this->throwException(new \InvalidArgumentException)); + + $this->indexManager->expects($this->never()) + ->method('getIndex'); + + $this->resetter->resetIndexType('index', 'nonExistant'); + } + + public function testPostPopulateWithoutAlias() + { + $this->mockIndex('index', new IndexConfig('index', array(), array())); + + $this->indexManager->expects($this->never()) + ->method('getIndex'); + $this->aliasProcessor->expects($this->never()) + ->method('switchIndexAlias'); + + $this->resetter->postPopulate('index'); + } + + public function testPostPopulate() + { + $indexConfig = new IndexConfig('index', array(), array( 'useAlias' => true)); + $index = $this->mockIndex('index', $indexConfig); + + $this->aliasProcessor->expects($this->once()) + ->method('switchIndexAlias') + ->with($indexConfig, $index); + + $this->resetter->postPopulate('index'); + } + + private function dispatcherExpects(array $events) + { + $expectation = $this->dispatcher->expects($this->exactly(count($events))) + ->method('dispatch'); + + call_user_func_array(array($expectation, 'withConsecutive'), $events); + } + + private function mockIndex($indexName, IndexConfig $config, $mapping = array()) + { + $this->configManager->expects($this->atLeast(1)) + ->method('getIndexConfiguration') + ->with($indexName) + ->will($this->returnValue($config)); + $index = new Index($this->elasticaClient, $indexName); + $this->indexManager->expects($this->any()) + ->method('getIndex') + ->with($indexName) + ->willReturn($index); + $this->mappingBuilder->expects($this->any()) + ->method('buildIndexMapping') + ->with($config) + ->willReturn($mapping); + + return $index; + } + + private function mockType($typeName, $indexName, TypeConfig $config, $mapping = array()) + { + $this->configManager->expects($this->atLeast(1)) + ->method('getTypeConfiguration') + ->with($indexName, $typeName) + ->will($this->returnValue($config)); + $index = new Index($this->elasticaClient, $indexName); + $this->indexManager->expects($this->once()) + ->method('getIndex') + ->with($indexName) + ->willReturn($index); + $this->mappingBuilder->expects($this->once()) + ->method('buildTypeMapping') + ->with($config) + ->willReturn($mapping); + + return $index; + } + + protected function setUp() + { + $this->aliasProcessor = $this->getMockBuilder('FOS\\ElasticaBundle\\Index\\AliasProcessor') + ->disableOriginalConstructor() + ->getMock(); $this->configManager = $this->getMockBuilder('FOS\\ElasticaBundle\\Configuration\\ConfigManager') ->disableOriginalConstructor() ->getMock(); - $this->indexManager = $this->getMockBuilder('FOS\\ElasticaBundle\\Index\\IndexManager') + $this->dispatcher = $this->getMockBuilder('Symfony\\Component\\EventDispatcher\\EventDispatcherInterface') + ->getMock(); + $this->elasticaClient = $this->getMockBuilder('Elastica\\Client') ->disableOriginalConstructor() ->getMock(); - $this->aliasProcessor = $this->getMockBuilder('FOS\\ElasticaBundle\\Index\\AliasProcessor') + $this->indexManager = $this->getMockBuilder('FOS\\ElasticaBundle\\Index\\IndexManager') ->disableOriginalConstructor() ->getMock(); $this->mappingBuilder = $this->getMockBuilder('FOS\\ElasticaBundle\\Index\\MappingBuilder') ->disableOriginalConstructor() ->getMock(); - $this->resetter = new Resetter($this->configManager, $this->indexManager, $this->aliasProcessor, $this->mappingBuilder); - - /*$this->indexConfigsByName = array( - 'foo' => array( - 'index' => $this->getMockElasticaIndex(), - 'config' => array( - 'properties' => array( - 'a' => array( - 'dynamic_templates' => array(), - 'properties' => array(), - ), - 'b' => array('properties' => array()), - ), - ), - ), - 'bar' => array( - 'index' => $this->getMockElasticaIndex(), - 'config' => array( - 'properties' => array( - 'a' => array('properties' => array()), - 'b' => array('properties' => array()), - ), - ), - ), - 'parent' => array( - 'index' => $this->getMockElasticaIndex(), - 'config' => array( - 'properties' => array( - 'a' => array( - 'properties' => array( - 'field_2' => array() - ), - '_parent' => array( - 'type' => 'b', - 'property' => 'b', - 'identifier' => 'id' - ), - ), - 'b' => array('properties' => array()), - ), - ), - ), - );*/ - } - - public function testResetAllIndexes() - { - $this->configManager->expects($this->once()) - ->method('getIndexNames') - ->will($this->returnValue(array('index1'))); - - $this->configManager->expects($this->once()) - ->method('getIndexConfiguration') - ->with('index1') - ->will($this->returnValue(new IndexConfig('index1', array(), array()))); - - $this->indexManager->expects($this->once()) - ->method('getIndex') - ->with('index1') - ->will($this->returnValue()); - - /*$this->indexConfigsByName['foo']['index']->expects($this->once()) - ->method('create') - ->with($this->indexConfigsByName['foo']['config'], true); - - $this->indexConfigsByName['bar']['index']->expects($this->once()) - ->method('create') - ->with($this->indexConfigsByName['bar']['config'], true); - - $resetter = new Resetter($this->indexConfigsByName);*/ - $this->resetter->resetAllIndexes(); - } - - public function testResetIndex() - { - $this->indexConfigsByName['foo']['index']->expects($this->once()) - ->method('create') - ->with($this->indexConfigsByName['foo']['config'], true); - - $this->indexConfigsByName['bar']['index']->expects($this->never()) - ->method('create'); - - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndex('foo'); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testResetIndexShouldThrowExceptionForInvalidIndex() - { - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndex('baz'); - } - - public function testResetIndexType() - { - $type = $this->getMockElasticaType(); - - $this->indexConfigsByName['foo']['index']->expects($this->once()) - ->method('getType') - ->with('a') - ->will($this->returnValue($type)); - - $type->expects($this->once()) - ->method('delete'); - - $mapping = Mapping::create($this->indexConfigsByName['foo']['config']['properties']['a']['properties']); - $mapping->setParam('dynamic_templates', $this->indexConfigsByName['foo']['config']['properties']['a']['dynamic_templates']); - $type->expects($this->once()) - ->method('setMapping') - ->with($mapping); - - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndexType('foo', 'a'); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testResetIndexTypeShouldThrowExceptionForInvalidIndex() - { - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndexType('baz', 'a'); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testResetIndexTypeShouldThrowExceptionForInvalidType() - { - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndexType('foo', 'c'); - } - - public function testResetIndexTypeIgnoreTypeMissingException() - { - $type = $this->getMockElasticaType(); - - $this->indexConfigsByName['foo']['index']->expects($this->once()) - ->method('getType') - ->with('a') - ->will($this->returnValue($type)); - - $type->expects($this->once()) - ->method('delete') - ->will($this->throwException(new ResponseException( - new Request(''), - new Response(array('error' => 'TypeMissingException[[de_20131022] type[bla] missing]', 'status' => 404))) - )); - - $mapping = Mapping::create($this->indexConfigsByName['foo']['config']['properties']['a']['properties']); - $mapping->setParam('dynamic_templates', $this->indexConfigsByName['foo']['config']['properties']['a']['dynamic_templates']); - $type->expects($this->once()) - ->method('setMapping') - ->with($mapping); - - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndexType('foo', 'a'); - } - - public function testIndexMappingForParent() - { - $type = $this->getMockElasticaType(); - - $this->indexConfigsByName['parent']['index']->expects($this->once()) - ->method('getType') - ->with('a') - ->will($this->returnValue($type)); - - $type->expects($this->once()) - ->method('delete'); - - $mapping = Mapping::create($this->indexConfigsByName['parent']['config']['properties']['a']['properties']); - $mapping->setParam('_parent', array('type' => 'b')); - $type->expects($this->once()) - ->method('setMapping') - ->with($mapping); - - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndexType('parent', 'a'); - } - - /** - * @return \Elastica\Index - */ - private function getMockElasticaIndex() - { - return $this->getMockBuilder('Elastica\Index') - ->disableOriginalConstructor() - ->getMock(); - } - - /** - * @return \Elastica\Type - */ - private function getMockElasticaType() - { - return $this->getMockBuilder('Elastica\Type') - ->disableOriginalConstructor() - ->getMock(); + $this->resetter = new Resetter( + $this->configManager, + $this->indexManager, + $this->aliasProcessor, + $this->mappingBuilder, + $this->dispatcher + ); } } diff --git a/Tests/Integration/MappingTest.php b/Tests/Integration/MappingTest.php index be134ed..ae7e409 100644 --- a/Tests/Integration/MappingTest.php +++ b/Tests/Integration/MappingTest.php @@ -8,10 +8,8 @@ * with this source code in the file LICENSE. */ - namespace FOS\ElasticaBundle\Tests\Integration; - -class MappingTest { - -} \ No newline at end of file +class MappingTest +{ +} diff --git a/Tests/Logger/ElasticaLoggerTest.php b/Tests/Logger/ElasticaLoggerTest.php index 96adf53..7d90639 100644 --- a/Tests/Logger/ElasticaLoggerTest.php +++ b/Tests/Logger/ElasticaLoggerTest.php @@ -22,7 +22,8 @@ class ElasticaLoggerTest extends \PHPUnit_Framework_TestCase /** * @param string $level * @param string $message - * @param array $context + * @param array $context + * * @return ElasticaLogger */ private function getMockLoggerForLevelMessageAndContext($level, $message, $context) @@ -45,7 +46,7 @@ class ElasticaLoggerTest extends \PHPUnit_Framework_TestCase public function testGetZeroIfNoQueriesAdded() { - $elasticaLogger = new ElasticaLogger; + $elasticaLogger = new ElasticaLogger(); $this->assertEquals(0, $elasticaLogger->getNbQueries()); } diff --git a/Tests/Manager/RepositoryManagerTest.php b/Tests/Manager/RepositoryManagerTest.php index 8849035..71bb076 100644 --- a/Tests/Manager/RepositoryManagerTest.php +++ b/Tests/Manager/RepositoryManagerTest.php @@ -4,16 +4,19 @@ namespace FOS\ElasticaBundle\Tests\Manager; use FOS\ElasticaBundle\Manager\RepositoryManager; -class CustomRepository{} +class CustomRepository +{ +} -class Entity{} +class Entity +{ +} /** * @author Richard Miller */ class RepositoryManagerTest extends \PHPUnit_Framework_TestCase { - public function testThatGetRepositoryReturnsDefaultRepository() { /** @var $finderMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ diff --git a/Tests/Persister/ObjectPersisterTest.php b/Tests/Persister/ObjectPersisterTest.php index 77a8809..06039f0 100644 --- a/Tests/Persister/ObjectPersisterTest.php +++ b/Tests/Persister/ObjectPersisterTest.php @@ -31,13 +31,6 @@ class InvalidObjectPersister extends ObjectPersister class ObjectPersisterTest extends \PHPUnit_Framework_TestCase { - public function setUp() - { - if (!class_exists('Elastica\Type')) { - $this->markTestSkipped('The Elastica library classes are not available'); - } - } - public function testThatCanReplaceObject() { $transformer = $this->getTransformer(); @@ -210,7 +203,7 @@ class ObjectPersisterTest extends \PHPUnit_Framework_TestCase private function getTransformer() { $transformer = new ModelToElasticaAutoTransformer(); - $transformer->setPropertyAccessor(PropertyAccess::getPropertyAccessor()); + $transformer->setPropertyAccessor(PropertyAccess::createPropertyAccessor()); return $transformer; } diff --git a/Tests/Persister/ObjectSerializerPersisterTest.php b/Tests/Persister/ObjectSerializerPersisterTest.php index fe15c0c..0536d06 100644 --- a/Tests/Persister/ObjectSerializerPersisterTest.php +++ b/Tests/Persister/ObjectSerializerPersisterTest.php @@ -2,9 +2,7 @@ namespace FOS\ElasticaBundle\Tests\ObjectSerializerPersister; -use FOS\ElasticaBundle\Persister\ObjectPersister; use FOS\ElasticaBundle\Persister\ObjectSerializerPersister; -use FOS\ElasticaBundle\Transformer\ModelToElasticaAutoTransformer; use FOS\ElasticaBundle\Transformer\ModelToElasticaIdentifierTransformer; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -26,13 +24,6 @@ class POPO class ObjectSerializerPersisterTest extends \PHPUnit_Framework_TestCase { - public function setUp() - { - if (!class_exists('Elastica\Type')) { - $this->markTestSkipped('The Elastica library classes are not available'); - } - } - public function testThatCanReplaceObject() { $transformer = $this->getTransformer(); @@ -121,7 +112,7 @@ class ObjectSerializerPersisterTest extends \PHPUnit_Framework_TestCase private function getTransformer() { $transformer = new ModelToElasticaIdentifierTransformer(); - $transformer->setPropertyAccessor(PropertyAccess::getPropertyAccessor()); + $transformer->setPropertyAccessor(PropertyAccess::createPropertyAccessor()); return $transformer; } diff --git a/Tests/Provider/IndexableTest.php b/Tests/Provider/IndexableTest.php index 6ef5669..e122ec1 100644 --- a/Tests/Provider/IndexableTest.php +++ b/Tests/Provider/IndexableTest.php @@ -21,7 +21,7 @@ class IndexableTest extends \PHPUnit_Framework_TestCase public function testIndexableUnknown() { $indexable = new Indexable(array(), $this->container); - $index = $indexable->isObjectIndexable('index', 'type', new Entity); + $index = $indexable->isObjectIndexable('index', 'type', new Entity()); $this->assertTrue($index); } @@ -32,9 +32,9 @@ class IndexableTest extends \PHPUnit_Framework_TestCase public function testValidIndexableCallbacks($callback, $return) { $indexable = new Indexable(array( - 'index/type' => $callback + 'index/type' => $callback, ), $this->container); - $index = $indexable->isObjectIndexable('index', 'type', new Entity); + $index = $indexable->isObjectIndexable('index', 'type', new Entity()); $this->assertEquals($return, $index); } @@ -46,15 +46,16 @@ class IndexableTest extends \PHPUnit_Framework_TestCase public function testInvalidIsIndexableCallbacks($callback) { $indexable = new Indexable(array( - 'index/type' => $callback + 'index/type' => $callback, ), $this->container); - $indexable->isObjectIndexable('index', 'type', new Entity); + $indexable->isObjectIndexable('index', 'type', new Entity()); } public function provideInvalidIsIndexableCallbacks() { return array( array('nonexistentEntityMethod'), + array(array('@indexableService', 'internalMethod')), array(array(new IndexableDecider(), 'internalMethod')), array(42), array('entity.getIsIndexable() && nonexistentEntityFunction()'), @@ -67,10 +68,13 @@ class IndexableTest extends \PHPUnit_Framework_TestCase array('isIndexable', false), array(array(new IndexableDecider(), 'isIndexable'), true), array(array('@indexableService', 'isIndexable'), true), - array(function(Entity $entity) { return $entity->maybeIndex(); }, true), + array(array('@indexableService'), true), + array(function (Entity $entity) { return $entity->maybeIndex(); }, true), array('entity.maybeIndex()', true), array('!object.isIndexable() && entity.property == "abc"', true), array('entity.property != "abc"', false), + array('["array", "values"]', true), + array('[]', false) ); } @@ -111,4 +115,9 @@ class IndexableDecider protected function internalMethod() { } + + public function __invoke($object) + { + return true; + } } diff --git a/Tests/RepositoryTest.php b/Tests/RepositoryTest.php index c4d4efc..7702af2 100644 --- a/Tests/RepositoryTest.php +++ b/Tests/RepositoryTest.php @@ -13,14 +13,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase { $testQuery = 'Test Query'; - /** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ - $finderMock = $this->getMockBuilder('FOS\ElasticaBundle\Finder\TransformedFinder') - ->disableOriginalConstructor() - ->getMock(); - $finderMock->expects($this->once()) - ->method('find') - ->with($this->equalTo($testQuery)); - + $finderMock = $this->getFinderMock($testQuery); $repository = new Repository($finderMock); $repository->find($testQuery); } @@ -30,14 +23,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase $testQuery = 'Test Query'; $testLimit = 20; - /** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ - $finderMock = $this->getMockBuilder('FOS\ElasticaBundle\Finder\TransformedFinder') - ->disableOriginalConstructor() - ->getMock(); - $finderMock->expects($this->once()) - ->method('find') - ->with($this->equalTo($testQuery), $this->equalTo($testLimit)); - + $finderMock = $this->getFinderMock($testQuery, $testLimit); $repository = new Repository($finderMock); $repository->find($testQuery, $testLimit); } @@ -46,14 +32,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase { $testQuery = 'Test Query'; - /** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ - $finderMock = $this->getMockBuilder('FOS\ElasticaBundle\Finder\TransformedFinder') - ->disableOriginalConstructor() - ->getMock(); - $finderMock->expects($this->once()) - ->method('findPaginated') - ->with($this->equalTo($testQuery)); - + $finderMock = $this->getFinderMock($testQuery, array(), 'findPaginated'); $repository = new Repository($finderMock); $repository->findPaginated($testQuery); } @@ -62,14 +41,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase { $testQuery = 'Test Query'; - /** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ - $finderMock = $this->getMockBuilder('FOS\ElasticaBundle\Finder\TransformedFinder') - ->disableOriginalConstructor() - ->getMock(); - $finderMock->expects($this->once()) - ->method('createPaginatorAdapter') - ->with($this->equalTo($testQuery)); - + $finderMock = $this->getFinderMock($testQuery, array(), 'createPaginatorAdapter'); $repository = new Repository($finderMock); $repository->createPaginatorAdapter($testQuery); } @@ -77,17 +49,28 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase public function testThatFindHybridCallsFindHybridOnFinder() { $testQuery = 'Test Query'; - $testLimit = 20; - /** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ + $finderMock = $this->getFinderMock($testQuery, null, 'findHybrid'); + $repository = new Repository($finderMock); + $repository->findHybrid($testQuery); + } + + /** + * @param string $testQuery + * @param mixed $testLimit + * @param string $method + * + * @return \FOS\ElasticaBundle\Finder\TransformedFinder + */ + private function getFinderMock($testQuery, $testLimit = null, $method = 'find') + { $finderMock = $this->getMockBuilder('FOS\ElasticaBundle\Finder\TransformedFinder') ->disableOriginalConstructor() ->getMock(); $finderMock->expects($this->once()) - ->method('findHybrid') + ->method($method) ->with($this->equalTo($testQuery), $this->equalTo($testLimit)); - $repository = new Repository($finderMock); - $repository->findHybrid($testQuery, $testLimit); + return $finderMock; } } diff --git a/Tests/Transformer/ElasticaToModelTransformerCollectionTest.php b/Tests/Transformer/ElasticaToModelTransformerCollectionTest.php index eb4d8e4..56a7200 100644 --- a/Tests/Transformer/ElasticaToModelTransformerCollectionTest.php +++ b/Tests/Transformer/ElasticaToModelTransformerCollectionTest.php @@ -37,7 +37,7 @@ class ElasticaToModelTransformerCollectionTest extends \PHPUnit_Framework_TestCa $this->collection = new ElasticaToModelTransformerCollection($this->transformers = array( 'type1' => $transformer1, 'type2' => $transformer2, - ), array()); + )); } public function testGetObjectClass() @@ -47,7 +47,7 @@ class ElasticaToModelTransformerCollectionTest extends \PHPUnit_Framework_TestCa $objectClasses = $this->collection->getObjectClass(); $this->assertEquals(array( 'type1' => 'FOS\ElasticaBundle\Tests\Transformer\POPO', - 'type2' => 'FOS\ElasticaBundle\Tests\Transformer\POPO2' + 'type2' => 'FOS\ElasticaBundle\Tests\Transformer\POPO2', ), $objectClasses); } @@ -89,8 +89,8 @@ class ElasticaToModelTransformerCollectionTest extends \PHPUnit_Framework_TestCa $this->transformers['type1']->expects($this->once()) ->method('transform') - ->with(array($document1,$document2)) - ->will($this->returnValue(array($result1,$result2))); + ->with(array($document1, $document2)) + ->will($this->returnValue(array($result1, $result2))); $results = $this->collection->transform(array($document1, $document2)); @@ -120,8 +120,8 @@ class ElasticaToModelTransformerCollectionTest extends \PHPUnit_Framework_TestCa return array( array( - $result, $transformedObject - ) + $result, $transformedObject, + ), ); } @@ -157,6 +157,9 @@ class POPO public $id; public $data; + /** + * @param integer $id + */ public function __construct($id, $data) { $this->data = $data; diff --git a/Tests/Transformer/ModelToElasticaAutoTransformerTest.php b/Tests/Transformer/ModelToElasticaAutoTransformerTest.php index 1fa6a8e..f45134e 100644 --- a/Tests/Transformer/ModelToElasticaAutoTransformerTest.php +++ b/Tests/Transformer/ModelToElasticaAutoTransformerTest.php @@ -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; @@ -21,8 +22,8 @@ class POPO public function __construct() { $this->date = new \DateTime('1979-05-05'); - $this->file = new \SplFileInfo(__DIR__ . '/../fixtures/attachment.odt'); - $this->fileContents = file_get_contents(__DIR__ . '/../fixtures/attachment.odt'); + $this->file = new \SplFileInfo(__DIR__.'/../fixtures/attachment.odt'); + $this->fileContents = file_get_contents(__DIR__.'/../fixtures/attachment.odt'); } public function getId() @@ -47,7 +48,7 @@ class POPO { return array( 'key1' => 'value1', - 'key2' => 'value2' + 'key2' => 'value2', ); } @@ -109,7 +110,7 @@ class POPO public function getNestedObject() { - return array('key1' => (object)array('id' => 1, 'key1sub1' => 'value1sub1', 'key1sub2' => 'value1sub2')); + return array('key1' => (object) array('id' => 1, 'key1sub1' => 'value1sub1', 'key1sub2' => 'value1sub2')); } public function getUpper() @@ -125,11 +126,33 @@ class POPO class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase { - public function setUp() + public function testTransformerDispatches() { - if (!class_exists('Elastica\Document')) { - $this->markTestSkipped('The Elastica library classes are not available'); - } + $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() @@ -152,7 +175,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase 'float' => array(), 'bool' => array(), 'date' => array(), - 'falseBool' => array() + 'falseBool' => array(), ) ); $data = $document->getData(); @@ -185,7 +208,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $this->assertEquals( array( 'key1' => 'value1', - 'key2' => 'value2' + 'key2' => 'value2', ), $data['array'] ); } @@ -230,7 +253,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $document = $transformer->transform(new POPO(), array('file' => array('type' => 'attachment'))); $data = $document->getData(); - $this->assertEquals(base64_encode(file_get_contents(__DIR__ . '/../fixtures/attachment.odt')), $data['file']); + $this->assertEquals(base64_encode(file_get_contents(__DIR__.'/../fixtures/attachment.odt')), $data['file']); } public function testFileContentsAddedForAttachmentMapping() @@ -240,7 +263,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $data = $document->getData(); $this->assertEquals( - base64_encode(file_get_contents(__DIR__ . '/../fixtures/attachment.odt')), $data['fileContents'] + base64_encode(file_get_contents(__DIR__.'/../fixtures/attachment.odt')), $data['fileContents'] ); } @@ -248,18 +271,18 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase { $transformer = $this->getTransformer(); $document = $transformer->transform(new POPO(), array( - 'sub' => array( - 'type' => 'nested', - 'properties' => array('foo' => '~') - ) - )); + 'sub' => array( + 'type' => 'nested', + 'properties' => array('foo' => array()), + ), + )); $data = $document->getData(); $this->assertTrue(array_key_exists('sub', $data)); $this->assertInternalType('array', $data['sub']); $this->assertEquals(array( array('foo' => 'foo'), - array('foo' => 'bar') + array('foo' => 'bar'), ), $data['sub']); } @@ -269,8 +292,8 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $document = $transformer->transform(new POPO(), array( 'sub' => array( 'type' => 'object', - 'properties' => array('bar') - ) + 'properties' => array('bar'), + ), )); $data = $document->getData(); @@ -278,7 +301,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $this->assertInternalType('array', $data['sub']); $this->assertEquals(array( array('bar' => 'foo'), - array('bar' => 'bar') + array('bar' => 'bar'), ), $data['sub']); } @@ -287,18 +310,18 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $transformer = $this->getTransformer(); $document = $transformer->transform(new POPO(), array( 'obj' => array( - 'type' => 'object' - ) + 'type' => 'object', + ), )); $data = $document->getData(); $this->assertTrue(array_key_exists('obj', $data)); $this->assertInternalType('array', $data['obj']); $this->assertEquals(array( - 'foo' => 'foo', - 'bar' => 'foo', - 'id' => 1 - ), $data['obj']); + 'foo' => 'foo', + 'bar' => 'foo', + 'id' => 1, + ), $data['obj']); } public function testObjectsMappingOfAtLeastOneAutoMappedObjectAndAtLeastOneManuallyMappedObject() @@ -313,14 +336,14 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase 'properties' => array( 'key1sub1' => array( 'type' => 'string', - 'properties' => array() + 'properties' => array(), ), 'key1sub2' => array( 'type' => 'string', - 'properties' => array() - ) - ) - ) + 'properties' => array(), + ), + ), + ), ) ); $data = $document->getData(); @@ -333,14 +356,14 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase array( 'foo' => 'foo', 'bar' => 'foo', - 'id' => 1 + 'id' => 1, ), $data['obj'] ); $this->assertEquals( array( 'key1sub1' => 'value1sub1', - 'key1sub2' => 'value1sub2' + 'key1sub2' => 'value1sub2', ), $data['nestedObject'][0] ); @@ -350,7 +373,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase { $transformer = $this->getTransformer(); $document = $transformer->transform(new POPO(), array( - '_parent' => array('type' => 'upper', 'property'=>'upper', 'identifier' => 'id'), + '_parent' => array('type' => 'upper', 'property' => 'upper', 'identifier' => 'id'), )); $this->assertEquals("parent", $document->getParent()); @@ -360,7 +383,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase { $transformer = $this->getTransformer(); $document = $transformer->transform(new POPO(), array( - '_parent' => array('type' => 'upper', 'property'=>'upper', 'identifier' => 'name'), + '_parent' => array('type' => 'upper', 'property' => 'upper', 'identifier' => 'name'), )); $this->assertEquals("a random name", $document->getParent()); @@ -370,7 +393,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase { $transformer = $this->getTransformer(); $document = $transformer->transform(new POPO(), array( - '_parent' => array('type' => 'upper', 'property'=>null, 'identifier' => 'id'), + '_parent' => array('type' => 'upper', 'property' => null, 'identifier' => 'id'), )); $this->assertEquals("parent", $document->getParent()); @@ -380,19 +403,21 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase { $transformer = $this->getTransformer(); $document = $transformer->transform(new POPO(), array( - '_parent' => array('type' => 'upper', 'property'=>'upperAlias', 'identifier' => 'id'), + '_parent' => array('type' => 'upper', 'property' => 'upperAlias', 'identifier' => 'id'), )); $this->assertEquals("parent", $document->getParent()); } /** + * @param null|\Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher + * * @return ModelToElasticaAutoTransformer */ - private function getTransformer() + private function getTransformer($dispatcher = null) { - $transformer = new ModelToElasticaAutoTransformer(); - $transformer->setPropertyAccessor(PropertyAccess::getPropertyAccessor()); + $transformer = new ModelToElasticaAutoTransformer(array(), $dispatcher); + $transformer->setPropertyAccessor(PropertyAccess::createPropertyAccessor()); return $transformer; } diff --git a/Tests/Transformer/ModelToElasticaIdentifierTransformerTest.php b/Tests/Transformer/ModelToElasticaIdentifierTransformerTest.php index f1a77d4..aa3d7b7 100644 --- a/Tests/Transformer/ModelToElasticaIdentifierTransformerTest.php +++ b/Tests/Transformer/ModelToElasticaIdentifierTransformerTest.php @@ -23,13 +23,6 @@ class POPO class ModelToElasticaIdentifierTransformerTest extends \PHPUnit_Framework_TestCase { - public function setUp() - { - if (!class_exists('Elastica\Document')) { - $this->markTestSkipped('The Elastica library classes are not available'); - } - } - public function testGetDocumentWithIdentifierOnly() { $transformer = $this->getTransformer(); @@ -58,7 +51,7 @@ class ModelToElasticaIdentifierTransformerTest extends \PHPUnit_Framework_TestCa private function getTransformer() { $transformer = new ModelToElasticaIdentifierTransformer(); - $transformer->setPropertyAccessor(PropertyAccess::getPropertyAccessor()); + $transformer->setPropertyAccessor(PropertyAccess::createPropertyAccessor()); return $transformer; } diff --git a/Transformer/AbstractElasticaToModelTransformer.php b/Transformer/AbstractElasticaToModelTransformer.php new file mode 100644 index 0000000..2b1de5c --- /dev/null +++ b/Transformer/AbstractElasticaToModelTransformer.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Transformer; + +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTransformerInterface +{ + /** + * PropertyAccessor instance. + * + * @var PropertyAccessorInterface + */ + protected $propertyAccessor; + + /** + * Set the PropertyAccessor instance. + * + * @param PropertyAccessorInterface $propertyAccessor + */ + public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor) + { + $this->propertyAccessor = $propertyAccessor; + } + + /** + * Returns a sorting closure to be used with usort() to put retrieved objects + * back in the order that they were returned by ElasticSearch. + * + * @param array $idPos + * @param string $identifierPath + * @return callable + */ + protected function getSortingClosure(array $idPos, $identifierPath) + { + $propertyAccessor = $this->propertyAccessor; + + return function ($a, $b) use ($idPos, $identifierPath, $propertyAccessor) { + return $idPos[$propertyAccessor->getValue($a, $identifierPath)] > $idPos[$propertyAccessor->getValue($b, $identifierPath)]; + }; + } +} diff --git a/Transformer/ElasticaToModelTransformerCollection.php b/Transformer/ElasticaToModelTransformerCollection.php index f65f8db..9920f43 100644 --- a/Transformer/ElasticaToModelTransformerCollection.php +++ b/Transformer/ElasticaToModelTransformerCollection.php @@ -41,6 +41,7 @@ class ElasticaToModelTransformerCollection implements ElasticaToModelTransformer /** * @param Document[] $elasticaObjects + * * @return array */ public function transform(array $elasticaObjects) @@ -51,12 +52,12 @@ class ElasticaToModelTransformerCollection implements ElasticaToModelTransformer } $transformed = array(); - foreach ($sorted AS $type => $objects) { + foreach ($sorted as $type => $objects) { $transformedObjects = $this->transformers[$type]->transform($objects); - $identifierGetter = 'get' . ucfirst($this->transformers[$type]->getIdentifierField()); + $identifierGetter = 'get'.ucfirst($this->transformers[$type]->getIdentifierField()); $transformed[$type] = array_combine( array_map( - function($o) use ($identifierGetter) { + function ($o) use ($identifierGetter) { return $o->$identifierGetter(); }, $transformedObjects @@ -80,7 +81,7 @@ class ElasticaToModelTransformerCollection implements ElasticaToModelTransformer $objects = $this->transform($elasticaObjects); $result = array(); - for ($i = 0; $i < count($elasticaObjects); $i++) { + for ($i = 0, $j = count($elasticaObjects); $i < $j; $i++) { $result[] = new HybridResult($elasticaObjects[$i], $objects[$i]); } diff --git a/Transformer/ElasticaToModelTransformerInterface.php b/Transformer/ElasticaToModelTransformerInterface.php index 5635ef3..71cd651 100644 --- a/Transformer/ElasticaToModelTransformerInterface.php +++ b/Transformer/ElasticaToModelTransformerInterface.php @@ -3,32 +3,33 @@ namespace FOS\ElasticaBundle\Transformer; /** - * Maps Elastica documents with model objects + * Maps Elastica documents with model objects. */ interface ElasticaToModelTransformerInterface { /** * Transforms an array of elastica objects into an array of - * model objects fetched from the doctrine repository + * model objects fetched from the doctrine repository. * * @param array $elasticaObjects array of elastica objects + * * @return array of model objects **/ - function transform(array $elasticaObjects); + public function transform(array $elasticaObjects); - function hybridTransform(array $elasticaObjects); + public function hybridTransform(array $elasticaObjects); /** * Returns the object class used by the transformer. * * @return string */ - function getObjectClass(); + public function getObjectClass(); /** - * Returns the identifier field from the options + * Returns the identifier field from the options. * * @return string the identifier field */ - function getIdentifierField(); + public function getIdentifierField(); } diff --git a/Transformer/HighlightableModelInterface.php b/Transformer/HighlightableModelInterface.php index d55407e..96c6c7c 100644 --- a/Transformer/HighlightableModelInterface.php +++ b/Transformer/HighlightableModelInterface.php @@ -1,16 +1,23 @@ - 'id' + 'identifier' => 'id', ); /** - * PropertyAccessor instance + * PropertyAccessor instance. * * @var PropertyAccessorInterface */ protected $propertyAccessor; /** - * Instanciates a new Mapper + * Instanciates a new Mapper. * - * @param array $options + * @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; } /** - * Set the PropertyAccessor + * Set the PropertyAccessor. * * @param PropertyAccessorInterface $propertyAccessor */ @@ -49,7 +58,7 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf } /** - * Transforms an object into an elastica object having the required keys + * Transforms an object into an elastica object having the required keys. * * @param object $object the object to convert * @param array $fields the keys we want to have in the returned array @@ -63,19 +72,27 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf foreach ($fields as $key => $mapping) { if ($key == '_parent') { - $property = (null !== $mapping['property'])?$mapping['property']:$mapping['type']; + $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,20 +103,28 @@ 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; } /** - * transform a nested document or an object property into an array of ElasticaDocument + * transform a nested document or an object property into an array of ElasticaDocument. * * @param array|\Traversable|\ArrayAccess $objects the object to convert - * @param array $fields the keys we want to have in the returned array + * @param array $fields the keys we want to have in the returned array * * @return array */ @@ -123,7 +148,7 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf } /** - * Attempts to convert any type to a string or an array of strings + * Attempts to convert any type to a string or an array of strings. * * @param mixed $value * @@ -131,12 +156,11 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf */ protected function normalizeValue($value) { - $normalizeValue = function(&$v) - { + $normalizeValue = function (&$v) { if ($v instanceof \DateTime) { $v = $v->format('c'); } elseif (!is_scalar($v) && !is_null($v)) { - $v = (string)$v; + $v = (string) $v; } }; diff --git a/Transformer/ModelToElasticaIdentifierTransformer.php b/Transformer/ModelToElasticaIdentifierTransformer.php index 7cf97e6..6301be1 100644 --- a/Transformer/ModelToElasticaIdentifierTransformer.php +++ b/Transformer/ModelToElasticaIdentifierTransformer.php @@ -6,12 +6,12 @@ use Elastica\Document; /** * Creates an Elastica document with the ID of - * the Doctrine object as Elastica document ID + * the Doctrine object as Elastica document ID. */ class ModelToElasticaIdentifierTransformer extends ModelToElasticaAutoTransformer { /** - * Creates an elastica document with the id of the doctrine object as id + * Creates an elastica document with the id of the doctrine object as id. * * @param object $object the object to convert * @param array $fields the keys we want to have in the returned array diff --git a/Transformer/ModelToElasticaTransformerInterface.php b/Transformer/ModelToElasticaTransformerInterface.php index ec9ada3..0ad9f12 100644 --- a/Transformer/ModelToElasticaTransformerInterface.php +++ b/Transformer/ModelToElasticaTransformerInterface.php @@ -3,16 +3,17 @@ namespace FOS\ElasticaBundle\Transformer; /** - * Maps Elastica documents with model objects + * Maps Elastica documents with model objects. */ interface ModelToElasticaTransformerInterface { /** - * Transforms an object into an elastica object having the required keys + * Transforms an object into an elastica object having the required keys. * * @param object $object the object to convert - * @param array $fields the keys we want to have in the returned array + * @param array $fields the keys we want to have in the returned array + * * @return \Elastica\Document **/ - function transform($object, array $fields); + public function transform($object, array $fields); } diff --git a/composer.json b/composer.json index bb30928..9705a04 100644 --- a/composer.json +++ b/composer.json @@ -17,36 +17,28 @@ "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":{ - "doctrine/orm": "~2.2", - "doctrine/doctrine-bundle": "~1.2@beta", + "doctrine/orm": "~2.4", + "doctrine/doctrine-bundle": "~1.2", "jms/serializer-bundle": "@stable", "phpunit/phpunit": "~4.1", "propel/propel1": "1.6.*", - "pagerfanta/pagerfanta": "1.0.*@dev", + "pagerfanta/pagerfanta": "~1.0", "knplabs/knp-components": "~1.2", "knplabs/knp-paginator-bundle": "~2.4", "symfony/browser-kit" : "~2.3", "symfony/expression-language" : "~2.4", "symfony/twig-bundle": "~2.3" }, - "suggest": { - "doctrine/orm": "~2.2", - "doctrine/mongodb-odm": "1.0.*@dev", - "propel/propel1": "1.6.*", - "pagerfanta/pagerfanta": "1.0.*@dev", - "knplabs/knp-components": "~1.2", - "symfony/expression-language" : "~2.4" - }, "autoload": { "psr-4": { "FOS\\ElasticaBundle\\": "" } }, "extra": { "branch-alias": { - "dev-master": "3.1.x-dev" + "dev-master": "3.2.x-dev" } } }