diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..e2cb043 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,5 @@ +imports: + - php + +tools: + external_code_coverage: true diff --git a/.travis.yml b/.travis.yml index 8f6a9d8..02f3ab8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,31 @@ language: php php: - 5.3 - - 5.4 + - 5.5 + - 5.6 + +matrix: + include: + - php: 5.5 + env: SYMFONY_VERSION='2.3.*' + - php: 5.5 + env: SYMFONY_VERSION='2.5.*' + - php: 5.5 + env: SYMFONY_VERSION='dev-master' before_script: - - echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - /usr/share/elasticsearch/bin/elasticsearch -v + - sudo /usr/share/elasticsearch/bin/plugin -install elasticsearch/elasticsearch-mapper-attachments/2.0.0 + - sudo service elasticsearch restart + - sh -c 'if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi;' + - 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 + +services: + - elasticsearch + +after_script: + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover coverage.clover diff --git a/Annotation/Search.php b/Annotation/Search.php new file mode 100644 index 0000000..26e1dbf --- /dev/null +++ b/Annotation/Search.php @@ -0,0 +1,16 @@ + + * @Annotation + * @Target("CLASS") + */ +class Search +{ + /** @var string */ + public $repositoryClass; +} diff --git a/CHANGELOG-3.0.md b/CHANGELOG-3.0.md index fd2ecd5..2596a29 100644 --- a/CHANGELOG-3.0.md +++ b/CHANGELOG-3.0.md @@ -12,12 +12,38 @@ 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.0-ALPHA3 (xxxx-xx-xx) +* 3.0.0-ALPHA6 + * Moved `is_indexable_callback` from the listener properties to a type property called + `indexable_callback` which is run when both populating and listening for object + changes. + * AbstractProvider constructor change: Second argument is now an `IndexableInterface` + instance. + * Annotation @Search moved to FOS\ElasticaBundle\Annotation\Search with FOS\ElasticaBundle\Configuration\Search deprecated + * Deprecated FOS\ElasticaBundle\Client in favour of FOS\ElasticaBundle\Elastica\Client + * Deprecated FOS\ElasticaBundle\DynamicIndex in favour of FOS\ElasticaBundle\Elastica\Index + * Deprecated FOS\ElasticaBundle\IndexManager in favour of FOS\ElasticaBundle\Index\IndexManager + * Deprecated FOS\ElasticaBundle\Resetter in favour of FOS\ElasticaBundle\Index\Resetter + +* 3.0.0-ALPHA5 (2014-05-23) + + * Doctrine Provider speed up by disabling persistence logging while populating documents + +* 3.0.0-ALPHA4 (2014-04-10) + + * Indexes are now capable of logging errors with Elastica + * Fixed deferred indexing of deleted documents + * Resetting an index will now create it even if it doesn't exist + * Bulk upserting of documents is now supported when populating + +* 3.0.0-ALPHA3 (2014-04-01) + + * a9c4c93: Logger is now only enabled in debug mode by default * #463: allowing hot swappable reindexing * #415: BC BREAK: document indexing occurs in postFlush rather than the pre* events previously. * 7d13823: Dropped (broken) support for Symfony <2.3 * #496: Added support for HTTP headers + * #528: FOSElasticaBundle will disable Doctrine logging when populating for a large increase in speed * 3.0.0-ALPHA2 (2014-03-17) diff --git a/CHANGELOG-3.1.md b/CHANGELOG-3.1.md new file mode 100644 index 0000000..ee9af70 --- /dev/null +++ b/CHANGELOG-3.1.md @@ -0,0 +1,16 @@ +CHANGELOG for 3.0.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.0 + +* BC BREAK: `DoctrineListener#scheduleForDeletion` access changed to private. +* BC BREAK: `ObjectPersisterInterface` gains the method `handlesObject` that + returns a boolean value if it will handle a given object or not. diff --git a/Client.php b/Client.php index 3eb98fe..d0cee46 100644 --- a/Client.php +++ b/Client.php @@ -2,43 +2,11 @@ namespace FOS\ElasticaBundle; -use Elastica\Client as ElasticaClient; -use Elastica\Request; -use FOS\ElasticaBundle\Logger\ElasticaLogger; +use FOS\ElasticaBundle\Elastica\Client as BaseClient; /** - * @author Gordon Franke + * @deprecated Use \FOS\ElasticaBundle\Elastica\LoggingClient */ -class Client extends ElasticaClient +class Client extends BaseClient { - /** - * {@inheritdoc} - */ - public function request($path, $method = Request::GET, $data = array(), array $query = array()) - { - $start = microtime(true); - $response = parent::request($path, $method, $data, $query); - - if (null !== $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->getConfig('headers'), - ); - - $this->_logger->logQuery($path, $method, $data, $time, $connection_array, $query); - } - - return $response; - } - - public function getIndex($name) - { - return new DynamicIndex($this, $name); - } } diff --git a/Command/PopulateCommand.php b/Command/PopulateCommand.php index 98834c7..f17ca4c 100644 --- a/Command/PopulateCommand.php +++ b/Command/PopulateCommand.php @@ -109,9 +109,9 @@ class PopulateCommand extends ContainerAwareCommand */ private function populateIndex(OutputInterface $output, $index, $reset, $options) { - if ($reset && $this->indexManager->getIndex($index)->exists()) { + if ($reset) { $output->writeln(sprintf('Resetting %s', $index)); - $this->resetter->resetIndex($index); + $this->resetter->resetIndex($index, true); } /** @var $providers ProviderInterface[] */ diff --git a/Command/ResetCommand.php b/Command/ResetCommand.php index 06cfe48..ce05e96 100755 --- a/Command/ResetCommand.php +++ b/Command/ResetCommand.php @@ -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/Configuration/ConfigManager.php b/Configuration/ConfigManager.php new file mode 100644 index 0000000..bef061b --- /dev/null +++ b/Configuration/ConfigManager.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Configuration; + +/** + * Central manager for index and type configuration. + */ +class ConfigManager implements ManagerInterface +{ + /** + * @var IndexConfig[] + */ + private $indexes = array(); + + /** + * @param Source\SourceInterface[] $sources + */ + public function __construct(array $sources) + { + foreach ($sources as $source) { + $this->indexes = array_merge($source->getConfiguration(), $this->indexes); + } + } + + public function getIndexConfiguration($indexName) + { + if (!$this->hasIndexConfiguration($indexName)) { + throw new \InvalidArgumentException(sprintf('Index with name "%s" is not configured.', $indexName)); + } + + return $this->indexes[$indexName]; + } + + public function getIndexNames() + { + return array_keys($this->indexes); + } + + public function getTypeConfiguration($indexName, $typeName) + { + $index = $this->getIndexConfiguration($indexName); + $type = $index->getType($typeName); + + if (!$type) { + throw new \InvalidArgumentException(sprintf('Type with name "%s" on index "%s" is not configured', $typeName, $indexName)); + } + + return $type; + } + + public function hasIndexConfiguration($indexName) + { + return isset($this->indexes[$indexName]); + } +} diff --git a/Configuration/IndexConfig.php b/Configuration/IndexConfig.php new file mode 100644 index 0000000..7416424 --- /dev/null +++ b/Configuration/IndexConfig.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Configuration; + +class IndexConfig +{ + /** + * The name of the index for ElasticSearch. + * + * @var string + */ + private $elasticSearchName; + + /** + * The internal name of the index. May not be the same as the name used in ElasticSearch, + * especially if aliases are enabled. + * + * @var string + */ + private $name; + + /** + * An array of settings sent to ElasticSearch when creating the index. + * + * @var array + */ + private $settings; + + /** + * All types that belong to this index. + * + * @var TypeConfig[] + */ + private $types; + + /** + * Indicates if the index should use an alias, allowing an index repopulation to occur + * without overwriting the current index. + * + * @var bool + */ + private $useAlias = false; + + /** + * Constructor expects an array as generated by the Container Configuration builder. + * + * @param string $name + * @param TypeConfig[] $types + * @param array $config + */ + public function __construct($name, array $types, array $config) + { + $this->elasticSearchName = isset($config['elasticSearchName']) ? $config['elasticSearchName'] : $name; + $this->name = $name; + $this->settings = isset($config['settings']) ? $config['settings'] : array(); + $this->types = $types; + $this->useAlias = isset($config['useAlias']) ? $config['useAlias'] : false; + } + + /** + * @return string + */ + public function getElasticSearchName() + { + return $this->elasticSearchName; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return array + */ + public function getSettings() + { + return $this->settings; + } + + /** + * @param string $typeName + * @return TypeConfig + * @throws \InvalidArgumentException + */ + public function getType($typeName) + { + if (!array_key_exists($typeName, $this->types)) { + throw new \InvalidArgumentException(sprintf('Type "%s" does not exist on index "%s"', $typeName, $this->name)); + } + + return $this->types[$typeName]; + } + + /** + * @return \FOS\ElasticaBundle\Configuration\TypeConfig[] + */ + public function getTypes() + { + return $this->types; + } + + /** + * @return boolean + */ + public function isUseAlias() + { + return $this->useAlias; + } +} diff --git a/Configuration/ManagerInterface.php b/Configuration/ManagerInterface.php new file mode 100644 index 0000000..96d510f --- /dev/null +++ b/Configuration/ManagerInterface.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Configuration; + +/** + * Central manager for index and type configuration. + */ +interface ManagerInterface +{ + /** + * Returns configuration for an index. + * + * @param $index + * @return IndexConfig + */ + public function getIndexConfiguration($index); + + /** + * Returns an array of known index names. + * + * @return array + */ + public function getIndexNames(); + + /** + * Returns a type configuration. + * + * @param string $index + * @param string $type + * @return TypeConfig + */ + public function getTypeConfiguration($index, $type); +} diff --git a/Configuration/Search.php b/Configuration/Search.php index cee10ab..1306f92 100644 --- a/Configuration/Search.php +++ b/Configuration/Search.php @@ -1,16 +1,25 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace FOS\ElasticaBundle\Configuration; +use FOS\ElasticaBundle\Annotation\Search as BaseSearch; + /** * Annotation class for setting search repository. * - * @author Richard Miller * @Annotation + * @deprecated Use FOS\ElasticaBundle\Annotation\Search instead * @Target("CLASS") */ -class Search +class Search extends BaseSearch { - /** @var string */ - public $repositoryClass; -} +} diff --git a/Configuration/Source/ContainerSource.php b/Configuration/Source/ContainerSource.php new file mode 100644 index 0000000..8d094c7 --- /dev/null +++ b/Configuration/Source/ContainerSource.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Configuration\Source; + +use FOS\ElasticaBundle\Configuration\IndexConfig; +use FOS\ElasticaBundle\Configuration\TypeConfig; + +/** + * Returns index and type configuration from the container. + */ +class ContainerSource implements SourceInterface +{ + /** + * The internal container representation of information. + * + * @var array + */ + private $configArray; + + public function __construct(array $configArray) + { + $this->configArray = $configArray; + } + + /** + * Should return all configuration available from the data source. + * + * @return IndexConfig[] + */ + public function getConfiguration() + { + $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.. + } + + $index = new IndexConfig($config['name'], $types, array( + 'elasticSearchName' => $config['elasticsearch_name'], + 'settings' => $config['settings'], + 'useAlias' => $config['use_alias'], + )); + + $indexes[$config['name']] = $index; + } + + return $indexes; + } +} diff --git a/Configuration/Source/SourceInterface.php b/Configuration/Source/SourceInterface.php new file mode 100644 index 0000000..34e9901 --- /dev/null +++ b/Configuration/Source/SourceInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Configuration\Source; + +/** + * Represents a source of index and type information (ie, the Container configuration or + * annotations). + */ +interface SourceInterface +{ + /** + * Should return all configuration available from the data source. + * + * @return \FOS\ElasticaBundle\Configuration\IndexConfig[] + */ + public function getConfiguration(); +} diff --git a/Configuration/TypeConfig.php b/Configuration/TypeConfig.php new file mode 100644 index 0000000..fc9041d --- /dev/null +++ b/Configuration/TypeConfig.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Configuration; + +class TypeConfig +{ + /** + * @var array + */ + private $config; + + /** + * @var array + */ + private $mapping; + + /** + * @var string + */ + private $name; + + public function __construct($name, array $mapping, array $config = array()) + { + $this->config = $config; + $this->mapping = $mapping; + $this->name = $name; + } + + /** + * @return string|null + */ + public function getIndexAnalyzer() + { + return $this->getConfig('index_analyzer'); + } + + /** + * @return array + */ + public function getMapping() + { + return $this->mapping; + } + + /** + * @return string|null + */ + public function getModel() + { + return isset($this->config['persistence']['model']) ? + $this->config['persistence']['model'] : + null; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string|null + */ + public function getSearchAnalyzer() + { + return $this->getConfig('search_analyzer'); + } + + /** + * @param string $key + */ + private function getConfig($key) + { + return isset($this->config[$key]) ? + $this->config[$key] : + null; + } +} diff --git a/DependencyInjection/Compiler/ConfigSourcePass.php b/DependencyInjection/Compiler/ConfigSourcePass.php new file mode 100644 index 0000000..b35a665 --- /dev/null +++ b/DependencyInjection/Compiler/ConfigSourcePass.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +class ConfigSourcePass implements CompilerPassInterface +{ + /** + * {@inheritDoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('fos_elastica.config_manager')) { + return; + } + + $sources = array(); + foreach (array_keys($container->findTaggedServiceIds('fos_elastica.config_source')) as $id) { + $sources[] = new Reference($id); + } + + $container->getDefinition('fos_elastica.config_manager')->replaceArgument(0, $sources); + } +} diff --git a/DependencyInjection/Compiler/IndexPass.php b/DependencyInjection/Compiler/IndexPass.php new file mode 100644 index 0000000..e131214 --- /dev/null +++ b/DependencyInjection/Compiler/IndexPass.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\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +class IndexPass implements CompilerPassInterface +{ + /** + * {@inheritDoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('fos_elastica.index_manager')) { + return; + } + + $indexes = array(); + foreach ($container->findTaggedServiceIds('fos_elastica.index') as $id => $tags) { + foreach ($tags as $tag) { + $indexes[$tag['name']] = new Reference($id); + } + } + + $container->getDefinition('fos_elastica.index_manager')->replaceArgument(0, $indexes); + } +} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index b7ef2ab..3c5d18c 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -8,19 +8,29 @@ use Symfony\Component\Config\Definition\ConfigurationInterface; class Configuration implements ConfigurationInterface { + /** + * Stores supported database drivers. + * + * @var array + */ private $supportedDrivers = array('orm', 'mongodb', 'propel'); - private $configArray = array(); + /** + * If the kernel is running in debug mode. + * + * @var bool + */ + private $debug; - public function __construct($configArray) + public function __construct($debug) { - $this->configArray = $configArray; + $this->debug = $debug; } /** * Generates the configuration tree. * - * @return \Symfony\Component\Config\Definition\NodeInterface + * @return TreeBuilder */ public function getConfigTreeBuilder() { @@ -32,8 +42,12 @@ class Configuration implements ConfigurationInterface $rootNode ->children() - ->scalarNode('default_client')->end() - ->scalarNode('default_index')->end() + ->scalarNode('default_client') + ->info('Defaults to the first client defined') + ->end() + ->scalarNode('default_index') + ->info('Defaults to the first index defined') + ->end() ->scalarNode('default_manager')->defaultValue('orm')->end() ->arrayNode('serializer') ->treatNullLike(array()) @@ -48,16 +62,6 @@ class Configuration implements ConfigurationInterface return $treeBuilder; } - /** - * Generates the configuration tree. - * - * @return \Symfony\Component\DependencyInjection\Configuration\NodeInterface - */ - public function getConfigTree() - { - return $this->getConfigTreeBuilder()->buildTree(); - } - /** * Adds the configuration for the "clients" key */ @@ -70,49 +74,42 @@ class Configuration implements ConfigurationInterface ->useAttributeAsKey('id') ->prototype('array') ->performNoDeepMerging() + // BC - Renaming 'servers' node to 'connections' ->beforeNormalization() - ->ifTrue(function($v) { return isset($v['host']) && isset($v['port']); }) - ->then(function($v) { - return array( - 'servers' => array( - array( - 'host' => $v['host'], - 'port' => $v['port'], - 'logger' => isset($v['logger']) ? $v['logger'] : null, - 'headers' => isset($v['headers']) ? $v['headers'] : null, - ) - ) - ); - }) + ->ifTrue(function($v) { return isset($v['servers']); }) + ->then(function($v) { + $v['connections'] = $v['servers']; + unset($v['servers']); + + return $v; + }) ->end() + // If there is no connections array key defined, assume a single connection. ->beforeNormalization() - ->ifTrue(function($v) { return isset($v['url']); }) - ->then(function($v) { - return array( - 'servers' => array( - array( - 'url' => $v['url'], - 'logger' => isset($v['logger']) ? $v['logger'] : null - ) - ) - ); - }) + ->ifTrue(function ($v) { return is_array($v) && !array_key_exists('connections', $v); }) + ->then(function ($v) { + return array( + 'connections' => array($v) + ); + }) ->end() ->children() - ->arrayNode('servers') + ->arrayNode('connections') + ->requiresAtLeastOneElement() ->prototype('array') ->fixXmlConfig('header') ->children() ->scalarNode('url') ->validate() - ->ifTrue(function($url) { return substr($url, -1) !== '/'; }) + ->ifTrue(function($url) { return $url && substr($url, -1) !== '/'; }) ->then(function($url) { return $url.'/'; }) ->end() ->end() ->scalarNode('host')->end() ->scalarNode('port')->end() + ->scalarNode('proxy')->end() ->scalarNode('logger') - ->defaultValue('fos_elastica.logger') + ->defaultValue($this->debug ? 'fos_elastica.logger' : false) ->treatNullLike('fos_elastica.logger') ->treatTrueLike('fos_elastica.logger') ->end() @@ -120,6 +117,7 @@ class Configuration implements ConfigurationInterface ->useAttributeAsKey('name') ->prototype('scalar')->end() ->end() + ->scalarNode('transport')->end() ->scalarNode('timeout')->end() ->end() ->end() @@ -145,7 +143,9 @@ class Configuration implements ConfigurationInterface ->useAttributeAsKey('name') ->prototype('array') ->children() - ->scalarNode('index_name')->end() + ->scalarNode('index_name') + ->info('Defaults to the name of the index, but can be modified if the index name is different in ElasticSearch') + ->end() ->booleanNode('use_alias')->defaultValue(false)->end() ->scalarNode('client')->end() ->scalarNode('finder') @@ -156,61 +156,8 @@ class Configuration implements ConfigurationInterface ->children() ->scalarNode('index_analyzer')->end() ->scalarNode('search_analyzer')->end() - ->arrayNode('persistence') - ->validate() - ->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']); }) - ->thenInvalid('Propel doesn\'t support the "repository" parameter') - ->end() - ->children() - ->scalarNode('driver') - ->validate() - ->ifNotInArray($this->supportedDrivers) - ->thenInvalid('The driver %s is not supported. Please choose one of '.json_encode($this->supportedDrivers)) - ->end() - ->end() - ->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('service')->end() - ->end() - ->end() - ->arrayNode('listener') - ->children() - ->scalarNode('insert')->defaultTrue()->end() - ->scalarNode('update')->defaultTrue()->end() - ->scalarNode('delete')->defaultTrue()->end() - ->scalarNode('persist')->defaultValue('postFlush')->end() - ->scalarNode('service')->end() - ->variableNode('is_indexable_callback')->defaultNull()->end() - ->end() - ->end() - ->arrayNode('finder') - ->children() - ->scalarNode('service')->end() - ->end() - ->end() - ->arrayNode('elastica_to_model_transformer') - ->addDefaultsIfNotSet() - ->children() - ->scalarNode('hydrate')->defaultTrue()->end() - ->scalarNode('ignore_missing')->defaultFalse()->end() - ->scalarNode('query_builder_method')->defaultValue('createQueryBuilder')->end() - ->scalarNode('service')->end() - ->end() - ->end() - ->arrayNode('model_to_elastica_transformer') - ->addDefaultsIfNotSet() - ->children() - ->scalarNode('service')->end() - ->end() - ->end() - ->end() - ->end() + ->append($this->getPersistenceNode()) + ->append($this->getSerializerNode()) ->end() ->end() ->variableNode('settings')->defaultValue(array())->end() @@ -234,79 +181,58 @@ class Configuration implements ConfigurationInterface ->useAttributeAsKey('name') ->prototype('array') ->treatNullLike(array()) + // BC - Renaming 'mappings' node to 'properties' + ->beforeNormalization() + ->ifTrue(function($v) { return array_key_exists('mappings', $v); }) + ->then(function($v) { + $v['properties'] = $v['mappings']; + unset($v['mappings']); + + return $v; + }) + ->end() + // BC - Support the old is_indexable_callback property + ->beforeNormalization() + ->ifTrue(function ($v) { + return isset($v['persistence']) && + isset($v['persistence']['listener']) && + isset($v['persistence']['listener']['is_indexable_callback']); + }) + ->then(function ($v) { + $v['indexable_callback'] = $v['persistence']['listener']['is_indexable_callback']; + unset($v['persistence']['listener']['is_indexable_callback']); + + return $v; + }) + ->end() + // Support multiple dynamic_template formats to match the old bundle style + // and the way ElasticSearch expects them + ->beforeNormalization() + ->ifTrue(function ($v) { return isset($v['dynamic_templates']); }) + ->then(function ($v) { + $dt = array(); + foreach ($v['dynamic_templates'] as $key => $type) { + if (is_int($key)) { + $dt[] = $type; + } else { + $dt[][$key] = $type; + } + } + + $v['dynamic_templates'] = $dt; + + return $v; + }) + ->end() ->children() - ->arrayNode('serializer') - ->addDefaultsIfNotSet() - ->children() - ->arrayNode('groups') - ->treatNullLike(array()) - ->prototype('scalar')->end() - ->end() - ->scalarNode('version')->end() - ->end() - ->end() ->scalarNode('index_analyzer')->end() ->scalarNode('search_analyzer')->end() - ->arrayNode('persistence') - ->validate() - ->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']); }) - ->thenInvalid('Propel doesn\'t support the "repository" parameter') - ->end() - ->children() - ->scalarNode('driver') - ->validate() - ->ifNotInArray($this->supportedDrivers) - ->thenInvalid('The driver %s is not supported. Please choose one of '.json_encode($this->supportedDrivers)) - ->end() - ->end() - ->scalarNode('model')->end() - ->scalarNode('repository')->end() - ->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('service')->end() - ->end() - ->end() - ->arrayNode('listener') - ->children() - ->scalarNode('insert')->defaultTrue()->end() - ->scalarNode('update')->defaultTrue()->end() - ->scalarNode('delete')->defaultTrue()->end() - ->booleanNode('immediate')->defaultFalse()->end() - ->scalarNode('service')->end() - ->variableNode('is_indexable_callback')->defaultNull()->end() - ->end() - ->end() - ->arrayNode('finder') - ->children() - ->scalarNode('service')->end() - ->end() - ->end() - ->arrayNode('elastica_to_model_transformer') - ->addDefaultsIfNotSet() - ->children() - ->scalarNode('hydrate')->defaultTrue()->end() - ->scalarNode('ignore_missing')->defaultFalse()->end() - ->scalarNode('query_builder_method')->defaultValue('createQueryBuilder')->end() - ->scalarNode('service')->end() - ->end() - ->end() - ->arrayNode('model_to_elastica_transformer') - ->addDefaultsIfNotSet() - ->children() - ->scalarNode('service')->end() - ->end() - ->end() - ->end() - ->end() + ->variableNode('indexable_callback')->end() + ->append($this->getPersistenceNode()) + ->append($this->getSerializerNode()) ->end() ->append($this->getIdNode()) - ->append($this->getMappingsNode()) + ->append($this->getPropertiesNode()) ->append($this->getDynamicTemplateNode()) ->append($this->getSourceNode()) ->append($this->getBoostNode()) @@ -322,27 +248,17 @@ class Configuration implements ConfigurationInterface } /** - * Returns the array node used for "mappings". + * Returns the array node used for "properties". */ - protected function getMappingsNode() + protected function getPropertiesNode() { $builder = new TreeBuilder(); - $node = $builder->root('mappings'); + $node = $builder->root('properties'); - $nestings = $this->getNestings(); - - $childrenNode = $node + $node ->useAttributeAsKey('name') - ->prototype('array') - ->validate() - ->ifTrue(function($v) { return isset($v['fields']) && empty($v['fields']); }) - ->then(function($v) { unset($v['fields']); return $v; }) - ->end() - ->treatNullLike(array()) - ->addDefaultsIfNotSet() - ->children(); - - $this->addFieldConfig($childrenNode, $nestings); + ->prototype('variable') + ->treatNullLike(array()); return $node; } @@ -356,205 +272,26 @@ class Configuration implements ConfigurationInterface $node = $builder->root('dynamic_templates'); $node - ->useAttributeAsKey('name') ->prototype('array') - ->children() - ->scalarNode('match')->end() - ->scalarNode('unmatch')->end() - ->scalarNode('match_mapping_type')->end() - ->scalarNode('path_match')->end() - ->scalarNode('path_unmatch')->end() - ->scalarNode('match_pattern')->end() - ->append($this->getDynamicTemplateMapping()) - ->end() - ->end() - ; - - return $node; - } - - /** - * @return the array node used for mapping in dynamic templates - */ - protected function getDynamicTemplateMapping() - { - $builder = new TreeBuilder(); - $node = $builder->root('mapping'); - - $nestings = $this->getNestingsForDynamicTemplates(); - - $this->addFieldConfig($node->children(), $nestings); - - return $node; - } - - /** - * @param \Symfony\Component\Config\Definition\Builder\NodeBuilder $node The node to which to attach the field config to - * @param array $nestings the nested mappings for the current field level - */ - protected function addFieldConfig($node, $nestings) - { - $node - ->scalarNode('type')->defaultValue('string')->end() - ->scalarNode('boost')->end() - ->scalarNode('store')->end() - ->scalarNode('index')->end() - ->scalarNode('index_analyzer')->end() - ->scalarNode('search_analyzer')->end() - ->scalarNode('analyzer')->end() - ->scalarNode('term_vector')->end() - ->scalarNode('null_value')->end() - ->booleanNode('include_in_all')->defaultValue(true)->end() - ->booleanNode('enabled')->defaultValue(true)->end() - ->scalarNode('lat_lon')->end() - ->scalarNode('index_name')->end() - ->booleanNode('omit_norms')->end() - ->scalarNode('index_options')->end() - ->scalarNode('ignore_above')->end() - ->scalarNode('position_offset_gap')->end() - ->arrayNode('_parent') - ->treatNullLike(array()) - ->children() - ->scalarNode('type')->end() - ->scalarNode('identifier')->defaultValue('id')->end() - ->end() - ->end() - ->scalarNode('format')->end() - ->scalarNode('similarity')->end(); - ; - - if (isset($nestings['fields'])) { - $this->addNestedFieldConfig($node, $nestings, 'fields'); - } - - if (isset($nestings['properties'])) { - $node - ->booleanNode('include_in_parent')->end() - ->booleanNode('include_in_root')->end() - ; - $this->addNestedFieldConfig($node, $nestings, 'properties'); - } - } - - /** - * @param \Symfony\Component\Config\Definition\Builder\NodeBuilder $node The node to which to attach the nested config to - * @param array $nestings The nestings for the current field level - * @param string $property the name of the nested property ('fields' or 'properties') - */ - protected function addNestedFieldConfig($node, $nestings, $property) - { - $childrenNode = $node - ->arrayNode($property) - ->useAttributeAsKey('name') ->prototype('array') - ->treatNullLike(array()) - ->addDefaultsIfNotSet() - ->children(); - - $this->addFieldConfig($childrenNode, $nestings[$property]); - - $childrenNode + ->children() + ->scalarNode('match')->end() + ->scalarNode('unmatch')->end() + ->scalarNode('match_mapping_type')->end() + ->scalarNode('path_match')->end() + ->scalarNode('path_unmatch')->end() + ->scalarNode('match_pattern')->end() + ->arrayNode('mapping') + ->prototype('variable') + ->treatNullLike(array()) + ->end() + ->end() ->end() ->end() ->end() ; - } - /** - * @return array The unique nested mappings for all types - */ - protected function getNestings() - { - if (!isset($this->configArray[0]['indexes'])) { - return array(); - } - - $nestings = array(); - foreach ($this->configArray[0]['indexes'] as $index) { - if (empty($index['types'])) { - continue; - } - - foreach ($index['types'] as $type) { - if (empty($type['mappings'])) { - continue; - } - - $nestings = array_merge_recursive($nestings, $this->getNestingsForType($type['mappings'], $nestings)); - } - } - return $nestings; - } - - /** - * @return array The unique nested mappings for all dynamic templates - */ - protected function getNestingsForDynamicTemplates() - { - if (!isset($this->configArray[0]['indexes'])) { - return array(); - } - - $nestings = array(); - foreach ($this->configArray[0]['indexes'] as $index) { - if (empty($index['types'])) { - continue; - } - - foreach ($index['types'] as $type) { - if (empty($type['dynamic_templates'])) { - continue; - } - - foreach ($type['dynamic_templates'] as $definition) { - $field = $definition['mapping']; - - if (isset($field['fields'])) { - $this->addPropertyNesting($field, $nestings, 'fields'); - } else if (isset($field['properties'])) { - $this->addPropertyNesting($field, $nestings, 'properties'); - } - } - - } - } - return $nestings; - } - - /** - * @param array $mappings The mappings for the current type - * @return array The nested mappings defined for this type - */ - protected function getNestingsForType(array $mappings = null) - { - if ($mappings === null) { - return array(); - } - - $nestings = array(); - - foreach ($mappings as $field) { - if (isset($field['fields'])) { - $this->addPropertyNesting($field, $nestings, 'fields'); - } else if (isset($field['properties'])) { - $this->addPropertyNesting($field, $nestings, 'properties'); - } - } - - return $nestings; - } - - /** - * @param array $field The field mapping definition - * @param array $nestings The nestings array - * @param string $property The nested property name ('fields' or 'properties') - */ - protected function addPropertyNesting($field, &$nestings, $property) - { - if (!isset($nestings[$property])) { - $nestings[$property] = array(); - } - $nestings[$property] = array_merge_recursive($nestings[$property], $this->getNestingsForType($field[$property])); + return $node; } /** @@ -594,7 +331,7 @@ class Configuration implements ConfigurationInterface ->end() ->scalarNode('compress')->end() ->scalarNode('compress_threshold')->end() - ->scalarNode('enabled')->end() + ->scalarNode('enabled')->defaultTrue()->end() ->end() ; @@ -715,4 +452,98 @@ class Configuration implements ConfigurationInterface return $node; } + + /** + * @return ArrayNodeDefinition|\Symfony\Component\Config\Definition\Builder\NodeDefinition + */ + protected function getPersistenceNode() + { + $builder = new TreeBuilder(); + $node = $builder->root('persistence'); + + $node + ->validate() + ->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']); }) + ->thenInvalid('Propel doesn\'t support the "repository" parameter') + ->end() + ->children() + ->scalarNode('driver') + ->validate() + ->ifNotInArray($this->supportedDrivers) + ->thenInvalid('The driver %s is not supported. Please choose one of '.json_encode($this->supportedDrivers)) + ->end() + ->end() + ->scalarNode('model')->end() + ->scalarNode('repository')->end() + ->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('service')->end() + ->end() + ->end() + ->arrayNode('listener') + ->children() + ->scalarNode('insert')->defaultTrue()->end() + ->scalarNode('update')->defaultTrue()->end() + ->scalarNode('delete')->defaultTrue()->end() + ->scalarNode('flush')->defaultTrue()->end() + ->booleanNode('immediate')->defaultFalse()->end() + ->scalarNode('logger') + ->defaultFalse() + ->treatNullLike('fos_elastica.logger') + ->treatTrueLike('fos_elastica.logger') + ->end() + ->scalarNode('service')->end() + ->end() + ->end() + ->arrayNode('finder') + ->children() + ->scalarNode('service')->end() + ->end() + ->end() + ->arrayNode('elastica_to_model_transformer') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('hydrate')->defaultTrue()->end() + ->scalarNode('ignore_missing')->defaultFalse()->end() + ->scalarNode('query_builder_method')->defaultValue('createQueryBuilder')->end() + ->scalarNode('service')->end() + ->end() + ->end() + ->arrayNode('model_to_elastica_transformer') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('service')->end() + ->end() + ->end() + ->end(); + + return $node; + } + + /** + * @return ArrayNodeDefinition|\Symfony\Component\Config\Definition\Builder\NodeDefinition + */ + protected function getSerializerNode() + { + $builder = new TreeBuilder(); + $node = $builder->root('serializer'); + + $node + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('groups') + ->treatNullLike(array()) + ->prototype('scalar')->end() + ->end() + ->scalarNode('version')->end() + ->end(); + + return $node; + } } diff --git a/DependencyInjection/FOSElasticaExtension.php b/DependencyInjection/FOSElasticaExtension.php index 553af14..804be44 100644 --- a/DependencyInjection/FOSElasticaExtension.php +++ b/DependencyInjection/FOSElasticaExtension.php @@ -4,9 +4,7 @@ namespace FOS\ElasticaBundle\DependencyInjection; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Config\FileLocator; @@ -14,21 +12,42 @@ use InvalidArgumentException; class FOSElasticaExtension extends Extension { - protected $indexConfigs = array(); - protected $typeFields = array(); - protected $loadedDrivers = array(); - protected $serializerConfig = array(); + /** + * Definition of elastica clients as configured by this extension. + * + * @var array + */ + private $clients = array(); + + /** + * An array of indexes as configured by the extension. + * + * @var array + */ + private $indexConfigs = array(); + + /** + * If we've encountered a type mapped to a specific persistence driver, it will be loaded + * here. + * + * @var array + */ + private $loadedDrivers = array(); public function load(array $configs, ContainerBuilder $container) { $configuration = $this->getConfiguration($configs, $container); - $config = $this->processConfiguration($configuration, $configs); + $config = $this->processConfiguration($configuration, $configs); $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); - $loader->load('config.xml'); if (empty($config['clients']) || empty($config['indexes'])) { - throw new InvalidArgumentException('You must define at least one client and one index'); + // No Clients or indexes are defined + return; + } + + foreach (array('config', 'index', 'persister', 'provider', 'source', 'transformer') as $basename) { + $loader->load(sprintf('%s.xml', $basename)); } if (empty($config['default_client'])) { @@ -41,25 +60,33 @@ class FOSElasticaExtension extends Extension $config['default_index'] = reset($keys); } - $clientIdsByName = $this->loadClients($config['clients'], $container); - $this->serializerConfig = isset($config['serializer']) ? $config['serializer'] : null; - $indexIdsByName = $this->loadIndexes($config['indexes'], $container, $clientIdsByName, $config['default_client']); - $indexRefsByName = array_map(function($id) { - return new Reference($id); - }, $indexIdsByName); + if (isset($config['serializer'])) { + $loader->load('serializer.xml'); - $this->loadIndexManager($indexRefsByName, $container); - $this->loadResetter($this->indexConfigs, $container); + $this->loadSerializer($config['serializer'], $container); + } + $this->loadClients($config['clients'], $container); $container->setAlias('fos_elastica.client', sprintf('fos_elastica.client.%s', $config['default_client'])); + + $this->loadIndexes($config['indexes'], $container); $container->setAlias('fos_elastica.index', sprintf('fos_elastica.index.%s', $config['default_index'])); + $container->getDefinition('fos_elastica.config_source.container')->replaceArgument(0, $this->indexConfigs); + + $this->loadIndexManager($container); + $this->createDefaultManagerAlias($config['default_manager'], $container); } + /** + * @param array $config + * @param ContainerBuilder $container + * @return Configuration + */ public function getConfiguration(array $config, ContainerBuilder $container) { - return new Configuration($config); + return new Configuration($container->getParameter('kernel.debug')); } /** @@ -69,13 +96,15 @@ class FOSElasticaExtension extends Extension * @param ContainerBuilder $container A ContainerBuilder instance * @return array */ - protected function loadClients(array $clients, ContainerBuilder $container) + private function loadClients(array $clients, ContainerBuilder $container) { - $clientIds = array(); foreach ($clients as $name => $clientConfig) { $clientId = sprintf('fos_elastica.client.%s', $name); - $clientDef = new Definition('%fos_elastica.client.class%', array($clientConfig)); - $logger = $clientConfig['servers'][0]['logger']; + + $clientDef = new DefinitionDecorator('fos_elastica.client_prototype'); + $clientDef->replaceArgument(0, $clientConfig); + + $logger = $clientConfig['connections'][0]['logger']; if (false !== $logger) { $clientDef->addMethodCall('setLogger', array(new Reference($logger))); } @@ -83,10 +112,11 @@ class FOSElasticaExtension extends Extension $container->setDefinition($clientId, $clientDef); - $clientIds[$name] = $clientId; + $this->clients[$name] = array( + 'id' => $clientId, + 'reference' => new Reference($clientId) + ); } - - return $clientIds; } /** @@ -94,56 +124,49 @@ class FOSElasticaExtension extends Extension * * @param array $indexes An array of indexes configurations * @param ContainerBuilder $container A ContainerBuilder instance - * @param array $clientIdsByName - * @param $defaultClientName - * @param $serializerConfig * @throws \InvalidArgumentException * @return array */ - protected function loadIndexes(array $indexes, ContainerBuilder $container, array $clientIdsByName, $defaultClientName) + private function loadIndexes(array $indexes, ContainerBuilder $container) { - $indexIds = array(); + $indexableCallbacks = array(); + foreach ($indexes as $name => $index) { - if (isset($index['client'])) { - $clientName = $index['client']; - if (!isset($clientIdsByName[$clientName])) { - throw new InvalidArgumentException(sprintf('The elastica client with name "%s" is not defined', $clientName)); - } - } else { - $clientName = $defaultClientName; - } - - $clientId = $clientIdsByName[$clientName]; $indexId = sprintf('fos_elastica.index.%s', $name); - $indexName = isset($index['index_name']) ? $index['index_name'] : $name; - $indexDefArgs = array($indexName); - $indexDef = new Definition('%fos_elastica.index.class%', $indexDefArgs); - $indexDef->setFactoryService($clientId); - $indexDef->setFactoryMethod('getIndex'); - $container->setDefinition($indexId, $indexDef); - $typePrototypeConfig = isset($index['type_prototype']) ? $index['type_prototype'] : array(); - $indexIds[$name] = $indexId; - $this->indexConfigs[$name] = array( - 'index' => new Reference($indexId), - 'name_or_alias' => $indexName, - 'config' => array( - 'mappings' => array() - ) - ); - if ($index['finder']) { - $this->loadIndexFinder($container, $name, $indexId); - } - if (!empty($index['settings'])) { - $this->indexConfigs[$name]['config']['settings'] = $index['settings']; - } - if ($index['use_alias']) { - $this->indexConfigs[$name]['use_alias'] = true; + $indexName = isset($index['index_name']) ? $index['index_name']: $name; + + $indexDef = new DefinitionDecorator('fos_elastica.index_prototype'); + $indexDef->replaceArgument(0, $indexName); + $indexDef->addTag('fos_elastica.index', array( + 'name' => $name, + )); + + if (isset($index['client'])) { + $client = $this->getClient($index['client']); + $indexDef->setFactoryService($client); } - $this->loadTypes(isset($index['types']) ? $index['types'] : array(), $container, $name, $indexId, $typePrototypeConfig); + $container->setDefinition($indexId, $indexDef); + $reference = new Reference($indexId); + + $this->indexConfigs[$name] = array( + 'elasticsearch_name' => $indexName, + 'reference' => $reference, + 'name' => $name, + 'settings' => $index['settings'], + 'type_prototype' => isset($index['type_prototype']) ? $index['type_prototype'] : array(), + 'use_alias' => $index['use_alias'], + ); + + if ($index['finder']) { + $this->loadIndexFinder($container, $name, $reference); + } + + $this->loadTypes((array) $index['types'], $container, $this->indexConfigs[$name], $indexableCallbacks); } - return $indexIds; + $indexable = $container->getDefinition('fos_elastica.indexable'); + $indexable->replaceArgument(0, $indexableCallbacks); } /** @@ -151,10 +174,10 @@ class FOSElasticaExtension extends Extension * * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container * @param string $name The index name - * @param string $indexId The index service identifier + * @param Reference $index Reference to the related index * @return string */ - protected function loadIndexFinder(ContainerBuilder $container, $name, $indexId) + private function loadIndexFinder(ContainerBuilder $container, $name, Reference $index) { /* Note: transformer services may conflict with "collection.index", if * an index and type names were "collection" and an index, respectively. @@ -165,166 +188,137 @@ class FOSElasticaExtension extends Extension $finderId = sprintf('fos_elastica.finder.%s', $name); $finderDef = new DefinitionDecorator('fos_elastica.finder'); - $finderDef->replaceArgument(0, new Reference($indexId)); + $finderDef->replaceArgument(0, $index); $finderDef->replaceArgument(1, new Reference($transformerId)); $container->setDefinition($finderId, $finderDef); - - return $finderId; } /** * Loads the configured types. * - * @param array $types An array of types configurations - * @param ContainerBuilder $container A ContainerBuilder instance - * @param $indexName - * @param $indexId - * @param array $typePrototypeConfig - * @param $serializerConfig + * @param array $types + * @param ContainerBuilder $container + * @param array $indexConfig + * @param array $indexableCallbacks */ - protected function loadTypes(array $types, ContainerBuilder $container, $indexName, $indexId, array $typePrototypeConfig) + private function loadTypes(array $types, ContainerBuilder $container, array $indexConfig, array &$indexableCallbacks) { foreach ($types as $name => $type) { - $type = self::deepArrayUnion($typePrototypeConfig, $type); - $typeId = sprintf('%s.%s', $indexId, $name); - $typeDefArgs = array($name); - $typeDef = new Definition('%fos_elastica.type.class%', $typeDefArgs); - $typeDef->setFactoryService($indexId); - $typeDef->setFactoryMethod('getType'); - if ($this->serializerConfig) { - $callbackDef = new Definition($this->serializerConfig['callback_class']); - $callbackId = sprintf('%s.%s.serializer.callback', $indexId, $name); + $indexName = $indexConfig['name']; - $typeDef->addMethodCall('setSerializer', array(array(new Reference($callbackId), 'serialize'))); - $callbackDef->addMethodCall('setSerializer', array(new Reference($this->serializerConfig['serializer']))); - if (isset($type['serializer']['groups'])) { - $callbackDef->addMethodCall('setGroups', array($type['serializer']['groups'])); - } - if (isset($type['serializer']['version'])) { - $callbackDef->addMethodCall('setVersion', array($type['serializer']['version'])); - } - $callbackClassImplementedInterfaces = class_implements($this->serializerConfig['callback_class']); // PHP < 5.4 friendly - if (isset($callbackClassImplementedInterfaces['Symfony\Component\DependencyInjection\ContainerAwareInterface'])) { - $callbackDef->addMethodCall('setContainer', array(new Reference('service_container'))); - } - - $container->setDefinition($callbackId, $callbackDef); - - $typeDef->addMethodCall('setSerializer', array(array(new Reference($callbackId), 'serialize'))); - } + $typeId = sprintf('%s.%s', $indexConfig['reference'], $name); + $typeDef = new DefinitionDecorator('fos_elastica.type_prototype'); + $typeDef->replaceArgument(0, $name); + $typeDef->setFactoryService($indexConfig['reference']); $container->setDefinition($typeId, $typeDef); - $this->indexConfigs[$indexName]['config']['mappings'][$name] = array( - "_source" => array("enabled" => true), // Add a default setting for empty mapping settings + $typeConfig = array( + 'name' => $name, + 'mapping' => array(), // An array containing anything that gets sent directly to ElasticSearch + 'config' => array(), ); - if (isset($type['_id'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['_id'] = $type['_id']; - } - if (isset($type['_source'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['_source'] = $type['_source']; - } - if (isset($type['_boost'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['_boost'] = $type['_boost']; - } - if (isset($type['_routing'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['_routing'] = $type['_routing']; - } - if (isset($type['mappings']) && !empty($type['mappings'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['properties'] = $type['mappings']; - $typeName = sprintf('%s/%s', $indexName, $name); - $this->typeFields[$typeName] = $type['mappings']; - } - if (isset($type['_parent'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['_parent'] = array('type' => $type['_parent']['type']); - $typeName = sprintf('%s/%s', $indexName, $name); - $this->typeFields[$typeName]['_parent'] = $type['_parent']; - } - if (isset($type['persistence'])) { - $this->loadTypePersistenceIntegration($type['persistence'], $container, $typeDef, $indexName, $name); - } - if (isset($type['index_analyzer'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['index_analyzer'] = $type['index_analyzer']; - } - if (isset($type['search_analyzer'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['search_analyzer'] = $type['search_analyzer']; - } - if (isset($type['index'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['index'] = $type['index']; - } - if (isset($type['_all'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['_all'] = $type['_all']; - } - if (isset($type['_timestamp'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['_timestamp'] = $type['_timestamp']; - } - if (isset($type['_ttl'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['_ttl'] = $type['_ttl']; - } - if (!empty($type['dynamic_templates'])) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['dynamic_templates'] = array(); - foreach ($type['dynamic_templates'] as $templateName => $templateData) { - $this->indexConfigs[$indexName]['config']['mappings'][$name]['dynamic_templates'][] = array($templateName => $templateData); + foreach (array( + 'dynamic_templates', + 'properties', + '_all', + '_boost', + '_id', + '_parent', + '_routing', + '_source', + '_timestamp', + '_ttl', + ) as $field) { + if (isset($type[$field])) { + $typeConfig['mapping'][$field] = $type[$field]; } } - } - } - /** - * Merges two arrays without reindexing numeric keys. - * - * @param array $array1 An array to merge - * @param array $array2 An array to merge - * - * @return array The merged array - */ - static protected function deepArrayUnion($array1, $array2) - { - foreach ($array2 as $key => $value) { - if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) { - $array1[$key] = self::deepArrayUnion($array1[$key], $value); - } else { - $array1[$key] = $value; + foreach (array( + 'persistence', + 'serializer', + 'index_analyzer', + 'search_analyzer', + ) as $field) { + $typeConfig['config'][$field] = array_key_exists($field, $type) ? + $type[$field] : + null; + } + + $this->indexConfigs[$indexName]['types'][$name] = $typeConfig; + + if (isset($type['persistence'])) { + $this->loadTypePersistenceIntegration($type['persistence'], $container, new Reference($typeId), $indexName, $name); + + $typeConfig['persistence'] = $type['persistence']; + } + + if (isset($type['indexable_callback'])) { + $indexableCallbacks[sprintf('%s/%s', $indexName, $name)] = $type['indexable_callback']; + } + + if ($container->hasDefinition('fos_elastica.serializer_callback_prototype')) { + $typeSerializerId = sprintf('%s.serializer.callback', $typeId); + $typeSerializerDef = new DefinitionDecorator('fos_elastica.serializer_callback_prototype'); + + if (isset($type['serializer']['groups'])) { + $typeSerializerDef->addMethodCall('setGroups', array($type['serializer']['groups'])); + } + if (isset($type['serializer']['version'])) { + $typeSerializerDef->addMethodCall('setVersion', array($type['serializer']['version'])); + } + + $typeDef->addMethodCall('setSerializer', array(array(new Reference($typeSerializerId), 'serialize'))); + $container->setDefinition($typeSerializerId, $typeSerializerDef); } } - - return $array1; } /** * Loads the optional provider and finder for a type * * @param array $typeConfig - * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container - * @param \Symfony\Component\DependencyInjection\Definition $typeDef - * @param $indexName - * @param $typeName + * @param ContainerBuilder $container + * @param Reference $typeRef + * @param string $indexName + * @param string $typeName */ - protected function loadTypePersistenceIntegration(array $typeConfig, ContainerBuilder $container, Definition $typeDef, $indexName, $typeName) + private function loadTypePersistenceIntegration(array $typeConfig, ContainerBuilder $container, Reference $typeRef, $indexName, $typeName) { $this->loadDriver($container, $typeConfig['driver']); $elasticaToModelTransformerId = $this->loadElasticaToModelTransformer($typeConfig, $container, $indexName, $typeName); $modelToElasticaTransformerId = $this->loadModelToElasticaTransformer($typeConfig, $container, $indexName, $typeName); - $objectPersisterId = $this->loadObjectPersister($typeConfig, $typeDef, $container, $indexName, $typeName, $modelToElasticaTransformerId); + $objectPersisterId = $this->loadObjectPersister($typeConfig, $typeRef, $container, $indexName, $typeName, $modelToElasticaTransformerId); if (isset($typeConfig['provider'])) { - $this->loadTypeProvider($typeConfig, $container, $objectPersisterId, $typeDef, $indexName, $typeName); + $this->loadTypeProvider($typeConfig, $container, $objectPersisterId, $indexName, $typeName); } if (isset($typeConfig['finder'])) { - $this->loadTypeFinder($typeConfig, $container, $elasticaToModelTransformerId, $typeDef, $indexName, $typeName); + $this->loadTypeFinder($typeConfig, $container, $elasticaToModelTransformerId, $typeRef, $indexName, $typeName); } if (isset($typeConfig['listener'])) { - $this->loadTypeListener($typeConfig, $container, $objectPersisterId, $typeDef, $indexName, $typeName); + $this->loadTypeListener($typeConfig, $container, $objectPersisterId, $indexName, $typeName); } } - protected function loadElasticaToModelTransformer(array $typeConfig, ContainerBuilder $container, $indexName, $typeName) + /** + * Creates and loads an ElasticaToModelTransformer. + * + * @param array $typeConfig + * @param ContainerBuilder $container + * @param string $indexName + * @param string $typeName + * @return string + */ + private function loadElasticaToModelTransformer(array $typeConfig, ContainerBuilder $container, $indexName, $typeName) { if (isset($typeConfig['elastica_to_model_transformer']['service'])) { return $typeConfig['elastica_to_model_transformer']['service']; } + /* Note: transformer services may conflict with "prototype.driver", if * the index and type names were "prototype" and a driver, respectively. */ @@ -337,28 +331,32 @@ class FOSElasticaExtension extends Extension $argPos = ('propel' === $typeConfig['driver']) ? 0 : 1; $serviceDef->replaceArgument($argPos, $typeConfig['model']); - $serviceDef->replaceArgument($argPos + 1, array( - 'hydrate' => $typeConfig['elastica_to_model_transformer']['hydrate'], - 'identifier' => $typeConfig['identifier'], - 'ignore_missing' => $typeConfig['elastica_to_model_transformer']['ignore_missing'], - 'query_builder_method' => $typeConfig['elastica_to_model_transformer']['query_builder_method'] - )); + $serviceDef->replaceArgument($argPos + 1, array_merge($typeConfig['elastica_to_model_transformer'], array( + 'identifier' => $typeConfig['identifier'], + ))); $container->setDefinition($serviceId, $serviceDef); return $serviceId; } - protected function loadModelToElasticaTransformer(array $typeConfig, ContainerBuilder $container, $indexName, $typeName) + /** + * Creates and loads a ModelToElasticaTransformer for an index/type. + * + * @param array $typeConfig + * @param ContainerBuilder $container + * @param string $indexName + * @param string $typeName + * @return string + */ + private function loadModelToElasticaTransformer(array $typeConfig, ContainerBuilder $container, $indexName, $typeName) { if (isset($typeConfig['model_to_elastica_transformer']['service'])) { return $typeConfig['model_to_elastica_transformer']['service']; } - if ($this->serializerConfig) { - $abstractId = sprintf('fos_elastica.model_to_elastica_identifier_transformer'); - } else { - $abstractId = sprintf('fos_elastica.model_to_elastica_transformer'); - } + $abstractId = $container->hasDefinition('fos_elastica.serializer_callback_prototype') ? + 'fos_elastica.model_to_elastica_identifier_transformer' : + 'fos_elastica.model_to_elastica_transformer'; $serviceId = sprintf('fos_elastica.model_to_elastica_transformer.%s.%s', $indexName, $typeName); $serviceDef = new DefinitionDecorator($abstractId); @@ -370,22 +368,34 @@ class FOSElasticaExtension extends Extension return $serviceId; } - protected function loadObjectPersister(array $typeConfig, Definition $typeDef, ContainerBuilder $container, $indexName, $typeName, $transformerId) + /** + * Creates and loads an object persister for a type. + * + * @param array $typeConfig + * @param Reference $typeRef + * @param ContainerBuilder $container + * @param string $indexName + * @param string $typeName + * @param string $transformerId + * @return string + */ + private function loadObjectPersister(array $typeConfig, Reference $typeRef, ContainerBuilder $container, $indexName, $typeName, $transformerId) { $arguments = array( - $typeDef, + $typeRef, new Reference($transformerId), $typeConfig['model'], ); - if ($this->serializerConfig) { + if ($container->hasDefinition('fos_elastica.serializer_callback_prototype')) { $abstractId = 'fos_elastica.object_serializer_persister'; - $callbackId = sprintf('%s.%s.serializer.callback', $this->indexConfigs[$indexName]['index'], $typeName); + $callbackId = sprintf('%s.%s.serializer.callback', $this->indexConfigs[$indexName]['reference'], $typeName); $arguments[] = array(new Reference($callbackId), 'serialize'); } else { $abstractId = 'fos_elastica.object_persister'; - $arguments[] = $this->typeFields[sprintf('%s/%s', $indexName, $typeName)]; + $arguments[] = $this->indexConfigs[$indexName]['types'][$typeName]['mapping']['properties']; } + $serviceId = sprintf('fos_elastica.object_persister.%s.%s', $indexName, $typeName); $serviceDef = new DefinitionDecorator($abstractId); foreach ($arguments as $i => $argument) { @@ -397,11 +407,22 @@ class FOSElasticaExtension extends Extension return $serviceId; } - protected function loadTypeProvider(array $typeConfig, ContainerBuilder $container, $objectPersisterId, $typeDef, $indexName, $typeName) + /** + * Loads a provider for a type. + * + * @param array $typeConfig + * @param ContainerBuilder $container + * @param string $objectPersisterId + * @param string $indexName + * @param string $typeName + * @return string + */ + private function loadTypeProvider(array $typeConfig, ContainerBuilder $container, $objectPersisterId, $indexName, $typeName) { if (isset($typeConfig['provider']['service'])) { return $typeConfig['provider']['service']; } + /* Note: provider services may conflict with "prototype.driver", if the * index and type names were "prototype" and a driver, respectively. */ @@ -409,19 +430,33 @@ class FOSElasticaExtension extends Extension $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(1, $typeConfig['model']); + $providerDef->replaceArgument(2, $typeConfig['model']); // Propel provider can simply ignore Doctrine-specific options - $providerDef->replaceArgument(2, array_diff_key($typeConfig['provider'], array('service' => 1))); + $providerDef->replaceArgument(3, array_merge(array_diff_key($typeConfig['provider'], array('service' => 1)), array( + 'indexName' => $indexName, + 'typeName' => $typeName, + ))); $container->setDefinition($providerId, $providerDef); return $providerId; } - protected function loadTypeListener(array $typeConfig, ContainerBuilder $container, $objectPersisterId, $typeDef, $indexName, $typeName) + /** + * Loads doctrine listeners to handle indexing of new or updated objects. + * + * @param array $typeConfig + * @param ContainerBuilder $container + * @param string $objectPersisterId + * @param string $indexName + * @param string $typeName + * @return string + */ + private function loadTypeListener(array $typeConfig, ContainerBuilder $container, $objectPersisterId, $indexName, $typeName) { if (isset($typeConfig['listener']['service'])) { return $typeConfig['listener']['service']; } + /* Note: listener services may conflict with "prototype.driver", if the * index and type names were "prototype" and a driver, respectively. */ @@ -429,25 +464,21 @@ 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, $typeConfig['model']); - $listenerDef->replaceArgument(3, $typeConfig['identifier']); - $listenerDef->replaceArgument(2, $this->getDoctrineEvents($typeConfig)); + $listenerDef->replaceArgument(1, $this->getDoctrineEvents($typeConfig)); + $listenerDef->replaceArgument(3, array( + 'identifier' => $typeConfig['identifier'], + 'indexName' => $indexName, + 'typeName' => $typeName, + )); + if ($typeConfig['listener']['logger']) { + $listenerDef->replaceArgument(4, new Reference($typeConfig['listener']['logger'])); + } + switch ($typeConfig['driver']) { case 'orm': $listenerDef->addTag('doctrine.event_subscriber'); break; case 'mongodb': $listenerDef->addTag('doctrine_mongodb.odm.event_subscriber'); break; } - if (isset($typeConfig['listener']['is_indexable_callback'])) { - $callback = $typeConfig['listener']['is_indexable_callback']; - if (is_array($callback)) { - list($class) = $callback + array(null); - if (is_string($class) && !class_exists($class)) { - $callback[0] = new Reference($class); - } - } - - $listenerDef->addMethodCall('setIsIndexableCallback', array($callback)); - } $container->setDefinition($listenerId, $listenerDef); return $listenerId; @@ -458,9 +489,6 @@ class FOSElasticaExtension extends Extension */ private function getDoctrineEvents(array $typeConfig) { - // Flush always calls depending on actions scheduled in lifecycle listeners - $typeConfig['listener']['flush'] = true; - switch ($typeConfig['driver']) { case 'orm': $eventsClass = '\Doctrine\ORM\Events'; @@ -490,14 +518,25 @@ class FOSElasticaExtension extends Extension return $events; } - protected function loadTypeFinder(array $typeConfig, ContainerBuilder $container, $elasticaToModelId, $typeDef, $indexName, $typeName) + /** + * Loads a Type specific Finder. + * + * @param array $typeConfig + * @param ContainerBuilder $container + * @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) { if (isset($typeConfig['finder']['service'])) { $finderId = $typeConfig['finder']['service']; } else { $finderId = sprintf('fos_elastica.finder.%s.%s', $indexName, $typeName); $finderDef = new DefinitionDecorator('fos_elastica.finder'); - $finderDef->replaceArgument(0, $typeDef); + $finderDef->replaceArgument(0, $typeRef); $finderDef->replaceArgument(1, new Reference($elasticaToModelId)); $container->setDefinition($finderId, $finderDef); } @@ -516,39 +555,61 @@ class FOSElasticaExtension extends Extension /** * Loads the index manager * - * @param array $indexRefsByName * @param ContainerBuilder $container **/ - protected function loadIndexManager(array $indexRefsByName, ContainerBuilder $container) + private function loadIndexManager(ContainerBuilder $container) { + $indexRefs = array_map(function ($index) { + return $index['reference']; + }, $this->indexConfigs); + $managerDef = $container->getDefinition('fos_elastica.index_manager'); - $managerDef->replaceArgument(0, $indexRefsByName); - $managerDef->replaceArgument(1, new Reference('fos_elastica.index')); + $managerDef->replaceArgument(0, $indexRefs); } /** - * Loads the resetter + * Makes sure a specific driver has been loaded. * - * @param array $indexConfigs - * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * @param ContainerBuilder $container + * @param string $driver */ - protected function loadResetter(array $indexConfigs, ContainerBuilder $container) - { - $resetterDef = $container->getDefinition('fos_elastica.resetter'); - $resetterDef->replaceArgument(0, $indexConfigs); - } - - protected function loadDriver(ContainerBuilder $container, $driver) + private function loadDriver(ContainerBuilder $container, $driver) { if (in_array($driver, $this->loadedDrivers)) { return; } + $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load($driver.'.xml'); $this->loadedDrivers[] = $driver; } - protected function createDefaultManagerAlias($defaultManager, ContainerBuilder $container) + /** + * Loads and configures the serializer prototype. + * + * @param array $config + * @param ContainerBuilder $container + */ + private function loadSerializer($config, ContainerBuilder $container) + { + $container->setAlias('fos_elastica.serializer', $config['serializer']); + + $serializer = $container->getDefinition('fos_elastica.serializer_callback_prototype'); + $serializer->setClass($config['callback_class']); + + $callbackClassImplementedInterfaces = class_implements($config['callback_class']); + if (isset($callbackClassImplementedInterfaces['Symfony\Component\DependencyInjection\ContainerAwareInterface'])) { + $serializer->addMethodCall('setContainer', array(new Reference('service_container'))); + } + } + + /** + * Creates a default manager alias for defined default manager or the first loaded driver. + * + * @param string $defaultManager + * @param ContainerBuilder $container + */ + private function createDefaultManagerAlias($defaultManager, ContainerBuilder $container) { if (0 == count($this->loadedDrivers)) { return; @@ -564,4 +625,20 @@ class FOSElasticaExtension extends Extension $container->setAlias('fos_elastica.manager', sprintf('fos_elastica.manager.%s', $defaultManagerService)); } + + /** + * Returns a reference to a client given its configured name. + * + * @param string $clientName + * @return Reference + * @throws \InvalidArgumentException + */ + private function getClient($clientName) + { + if (!array_key_exists($clientName, $this->clients)) { + throw new InvalidArgumentException(sprintf('The elastica client with name "%s" is not defined', $clientName)); + } + + return $this->clients[$clientName]['reference']; + } } diff --git a/Doctrine/AbstractElasticaToModelTransformer.php b/Doctrine/AbstractElasticaToModelTransformer.php index 147067d..96f73bd 100755 --- a/Doctrine/AbstractElasticaToModelTransformer.php +++ b/Doctrine/AbstractElasticaToModelTransformer.php @@ -120,11 +120,17 @@ abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTran public function hybridTransform(array $elasticaObjects) { + $indexedElasticaResults = array(); + foreach ($elasticaObjects as $elasticaObject) { + $indexedElasticaResults[$elasticaObject->getId()] = $elasticaObject; + } + $objects = $this->transform($elasticaObjects); $result = array(); - for ($i = 0; $i < count($elasticaObjects); $i++) { - $result[] = new HybridResult($elasticaObjects[$i], $objects[$i]); + foreach ($objects as $object) { + $id = $this->propertyAccessor->getValue($object, $this->options['identifier']); + $result[] = new HybridResult($indexedElasticaResults[$id], $object); } return $result; diff --git a/Doctrine/AbstractProvider.php b/Doctrine/AbstractProvider.php index 9d1575c..a662fd4 100644 --- a/Doctrine/AbstractProvider.php +++ b/Doctrine/AbstractProvider.php @@ -6,6 +6,7 @@ use Doctrine\Common\Persistence\ManagerRegistry; use Elastica\Exception\Bulk\ResponseException as BulkResponseException; use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; use FOS\ElasticaBundle\Provider\AbstractProvider as BaseAbstractProvider; +use FOS\ElasticaBundle\Provider\IndexableInterface; abstract class AbstractProvider extends BaseAbstractProvider { @@ -15,14 +16,21 @@ abstract class AbstractProvider extends BaseAbstractProvider * Constructor. * * @param ObjectPersisterInterface $objectPersister - * @param string $objectClass - * @param array $options - * @param ManagerRegistry $managerRegistry + * @param IndexableInterface $indexable + * @param string $objectClass + * @param array $options + * @param ManagerRegistry $managerRegistry */ - public function __construct(ObjectPersisterInterface $objectPersister, $objectClass, array $options, $managerRegistry) - { - parent::__construct($objectPersister, $objectClass, array_merge(array( + public function __construct( + ObjectPersisterInterface $objectPersister, + IndexableInterface $indexable, + $objectClass, + array $options, + ManagerRegistry $managerRegistry + ) { + parent::__construct($objectPersister, $indexable, $objectClass, array_merge(array( 'clear_object_manager' => true, + 'debug_logging' => false, 'ignore_errors' => false, 'query_builder_method' => 'createQueryBuilder', ), $options)); @@ -35,18 +43,34 @@ abstract class AbstractProvider extends BaseAbstractProvider */ 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); @@ -61,19 +85,23 @@ abstract class AbstractProvider extends BaseAbstractProvider } if ($this->options['clear_object_manager']) { - $this->managerRegistry->getManagerForClass($this->objectClass)->clear(); + $manager->clear(); } usleep($sleep); if ($loggerClosure) { - $stepNbObjects = count($objects); $stepCount = $stepNbObjects + $offset; $percentComplete = 100 * $stepCount / $nbObjects; - $objectsPerSecond = $stepNbObjects / (microtime(true) - $stepStartTime); + $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); + } } /** @@ -84,6 +112,21 @@ abstract class AbstractProvider extends BaseAbstractProvider */ protected abstract function countObjects($queryBuilder); + /** + * Disables logging and returns the logger that was previously set. + * + * @return mixed + */ + protected abstract function disableLogging(); + + /** + * Reenables the logger with the previously returned logger from disableLogging(); + * + * @param mixed $logger + * @return mixed + */ + protected abstract function enableLogging($logger); + /** * Fetches a slice of objects using the query builder. * diff --git a/Doctrine/Listener.php b/Doctrine/Listener.php index c254513..039ddaa 100644 --- a/Doctrine/Listener.php +++ b/Doctrine/Listener.php @@ -2,14 +2,18 @@ namespace FOS\ElasticaBundle\Doctrine; -use Doctrine\Common\EventArgs; use Doctrine\Common\EventSubscriber; +use Doctrine\Common\Persistence\Event\LifecycleEventArgs; use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; use FOS\ElasticaBundle\Persister\ObjectPersister; -use Symfony\Component\ExpressionLanguage\Expression; -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -use Symfony\Component\ExpressionLanguage\SyntaxError; +use FOS\ElasticaBundle\Provider\IndexableInterface; +use Symfony\Component\PropertyAccess\PropertyAccess; +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 { /** @@ -19,13 +23,6 @@ class Listener implements EventSubscriber */ protected $objectPersister; - /** - * Class of the domain model - * - * @var string - */ - protected $objectClass; - /** * List of subscribed events * @@ -34,47 +31,72 @@ class Listener implements EventSubscriber protected $events; /** - * Name of domain model field used as the ES identifier + * Configuration for the listener * * @var string */ - protected $esIdentifierField; + private $config; /** - * Callback for determining if an object should be indexed + * Objects scheduled for insertion. * - * @var mixed - */ - protected $isIndexableCallback; - - /** - * Objects scheduled for insertion, replacement, or removal + * @var array */ public $scheduledForInsertion = array(); + + /** + * Objects scheduled to be updated or removed. + * + * @var array + */ public $scheduledForUpdate = array(); + + /** + * IDs of objects scheduled for removal + * + * @var array + */ public $scheduledForDeletion = array(); /** - * An instance of ExpressionLanguage + * PropertyAccessor instance * - * @var ExpressionLanguage + * @var PropertyAccessorInterface */ - protected $expressionLanguage; + protected $propertyAccessor; + + /** + * @var IndexableInterface + */ + private $indexable; /** * Constructor. * * @param ObjectPersisterInterface $objectPersister - * @param string $objectClass - * @param array $events - * @param string $esIdentifierField + * @param array $events + * @param IndexableInterface $indexable + * @param array $config + * @param null $logger */ - public function __construct(ObjectPersisterInterface $objectPersister, $objectClass, array $events, $esIdentifierField = 'id') - { - $this->objectPersister = $objectPersister; - $this->objectClass = $objectClass; - $this->events = $events; - $this->esIdentifierField = $esIdentifierField; + public function __construct( + ObjectPersisterInterface $objectPersister, + array $events, + IndexableInterface $indexable, + array $config = array(), + $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) { + $this->objectPersister->setLogger($logger); + } } /** @@ -86,160 +108,122 @@ class Listener implements EventSubscriber } /** - * Set the callback for determining object index eligibility. + * Looks for new objects that should be indexed. * - * If callback is a string, it must be public method on the object class - * that expects no arguments and returns a boolean. Otherwise, the callback - * should expect the object for consideration as its only argument and - * return a boolean. - * - * @param callback $callback - * @throws \RuntimeException if the callback is not callable + * @param LifecycleEventArgs $eventArgs */ - public function setIsIndexableCallback($callback) + public function postPersist(LifecycleEventArgs $eventArgs) { - if (is_string($callback)) { - if (!is_callable(array($this->objectClass, $callback))) { - if (false !== ($expression = $this->getExpressionLanguage())) { - $callback = new Expression($callback); - try { - $expression->compile($callback, array($this->getExpressionVar())); - } catch (SyntaxError $e) { - throw new \RuntimeException(sprintf('Indexable callback %s::%s() is not callable or a valid expression.', $this->objectClass, $callback), 0, $e); - } - } else { - throw new \RuntimeException(sprintf('Indexable callback %s::%s() is not callable.', $this->objectClass, $callback)); - } - } - } elseif (!is_callable($callback)) { - if (is_array($callback)) { - list($class, $method) = $callback + array(null, null); - if (is_object($class)) { - $class = get_class($class); - } + $entity = $eventArgs->getObject(); - if ($class && $method) { - throw new \RuntimeException(sprintf('Indexable callback %s::%s() is not callable.', $class, $method)); - } - } - throw new \RuntimeException('Indexable callback is not callable.'); - } - - $this->isIndexableCallback = $callback; - } - - /** - * Return whether the object is indexable with respect to the callback. - * - * @param object $object - * @return boolean - */ - protected function isObjectIndexable($object) - { - if (!$this->isIndexableCallback) { - return true; - } - - if ($this->isIndexableCallback instanceof Expression) { - return $this->getExpressionLanguage()->evaluate($this->isIndexableCallback, array($this->getExpressionVar($object) => $object)); - } - - return is_string($this->isIndexableCallback) - ? call_user_func(array($object, $this->isIndexableCallback)) - : call_user_func($this->isIndexableCallback, $object); - } - - /** - * @param mixed $object - * @return string - */ - private function getExpressionVar($object = null) - { - $class = $object ?: $this->objectClass; - $ref = new \ReflectionClass($class); - - return strtolower($ref->getShortName()); - } - - /** - * @return bool|ExpressionLanguage - */ - private function getExpressionLanguage() - { - if (null === $this->expressionLanguage) { - if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { - return false; - } - - $this->expressionLanguage = new ExpressionLanguage(); - } - - return $this->expressionLanguage; - } - - public function postPersist(EventArgs $eventArgs) - { - $entity = $eventArgs->getEntity(); - - if ($entity instanceof $this->objectClass && $this->isObjectIndexable($entity)) { + 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 = $eventArgs->getEntity(); + $entity = $eventArgs->getObject(); - if ($entity instanceof $this->objectClass) { + if ($this->objectPersister->handlesObject($entity)) { if ($this->isObjectIndexable($entity)) { $this->scheduledForUpdate[] = $entity; } else { // Delete if no longer indexable - $this->scheduledForDeletion[] = $entity; + $this->scheduleForDeletion($entity); } } } - public function preRemove(EventArgs $eventArgs) + /** + * 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 + * + * @param LifecycleEventArgs $eventArgs + */ + public function preRemove(LifecycleEventArgs $eventArgs) { - $entity = $eventArgs->getEntity(); + $entity = $eventArgs->getObject(); - if ($entity instanceof $this->objectClass) { - $this->scheduledForDeletion[] = $entity; + if ($this->objectPersister->handlesObject($entity)) { + $this->scheduleForDeletion($entity); } } /** * Persist scheduled objects to ElasticSearch + * After persisting, clear the scheduled queue to prevent multiple data updates when using multiple flush calls */ private function persistScheduled() { if (count($this->scheduledForInsertion)) { $this->objectPersister->insertMany($this->scheduledForInsertion); + $this->scheduledForInsertion = array(); } if (count($this->scheduledForUpdate)) { $this->objectPersister->replaceMany($this->scheduledForUpdate); + $this->scheduledForUpdate = array(); } if (count($this->scheduledForDeletion)) { - $this->objectPersister->deleteMany($this->scheduledForDeletion); + $this->objectPersister->deleteManyByIdentifiers($this->scheduledForDeletion); + $this->scheduledForDeletion = array(); } } /** - * 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 object $object + */ + private function scheduleForDeletion($object) + { + if ($identifierValue = $this->propertyAccessor->getValue($object, $this->config['identifier'])) { + $this->scheduledForDeletion[] = $identifierValue; + } + } + + /** + * Checks if the object is indexable or not. + * + * @param object $object + * @return bool + */ + private function isObjectIndexable($object) + { + return $this->indexable->isObjectIndexable( + $this->config['indexName'], + $this->config['typeName'], + $object + ); + } } diff --git a/Doctrine/MongoDB/ElasticaToModelTransformer.php b/Doctrine/MongoDB/ElasticaToModelTransformer.php index 855a093..cea737f 100644 --- a/Doctrine/MongoDB/ElasticaToModelTransformer.php +++ b/Doctrine/MongoDB/ElasticaToModelTransformer.php @@ -22,6 +22,7 @@ class ElasticaToModelTransformer extends AbstractElasticaToModelTransformer { return $this->registry ->getManagerForClass($this->objectClass) + ->getRepository($this->objectClass) ->{$this->options['query_builder_method']}($this->objectClass) ->field($this->options['identifier'])->in($identifierValues) ->hydrate($hydrate) diff --git a/Doctrine/MongoDB/Provider.php b/Doctrine/MongoDB/Provider.php index 16d9f76..9e1c5dd 100644 --- a/Doctrine/MongoDB/Provider.php +++ b/Doctrine/MongoDB/Provider.php @@ -8,6 +8,40 @@ use FOS\ElasticaBundle\Exception\InvalidArgumentTypeException; class Provider extends AbstractProvider { + /** + * Disables logging and returns the logger that was previously set. + * + * @return mixed + */ + protected function disableLogging() + { + $configuration = $this->managerRegistry + ->getManagerForClass($this->objectClass) + ->getConnection() + ->getConfiguration(); + + $logger = $configuration->getLoggerCallable(); + $configuration->setLoggerCallable(null); + + return $logger; + } + + /** + * Reenables the logger with the previously returned logger from disableLogging(); + * + * @param mixed $logger + * @return mixed + */ + protected function enableLogging($logger) + { + $configuration = $this->managerRegistry + ->getManagerForClass($this->objectClass) + ->getConnection() + ->getConfiguration(); + + $configuration->setLoggerCallable($logger); + } + /** * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::countObjects() */ diff --git a/Doctrine/ORM/ElasticaToModelTransformer.php b/Doctrine/ORM/ElasticaToModelTransformer.php index 20ec6e8..a57a84c 100644 --- a/Doctrine/ORM/ElasticaToModelTransformer.php +++ b/Doctrine/ORM/ElasticaToModelTransformer.php @@ -29,7 +29,7 @@ class ElasticaToModelTransformer extends AbstractElasticaToModelTransformer $hydrationMode = $hydrate ? Query::HYDRATE_OBJECT : Query::HYDRATE_ARRAY; $qb = $this->getEntityQueryBuilder(); - $qb->where($qb->expr()->in(static::ENTITY_ALIAS.'.'.$this->options['identifier'], ':values')) + $qb->andWhere($qb->expr()->in(static::ENTITY_ALIAS.'.'.$this->options['identifier'], ':values')) ->setParameter('values', $identifierValues); return $qb->getQuery()->setHydrationMode($hydrationMode)->execute(); diff --git a/Doctrine/ORM/Provider.php b/Doctrine/ORM/Provider.php index 3549550..7e2ac12 100644 --- a/Doctrine/ORM/Provider.php +++ b/Doctrine/ORM/Provider.php @@ -9,7 +9,41 @@ use FOS\ElasticaBundle\Exception\InvalidArgumentTypeException; class Provider extends AbstractProvider { const ENTITY_ALIAS = 'a'; - + + /** + * Disables logging and returns the logger that was previously set. + * + * @return mixed + */ + protected function disableLogging() + { + $configuration = $this->managerRegistry + ->getManagerForClass($this->objectClass) + ->getConnection() + ->getConfiguration(); + + $logger = $configuration->getSQLLogger(); + $configuration->setSQLLogger(null); + + return $logger; + } + + /** + * Reenables the logger with the previously returned logger from disableLogging(); + * + * @param mixed $logger + * @return mixed + */ + protected function enableLogging($logger) + { + $configuration = $this->managerRegistry + ->getManagerForClass($this->objectClass) + ->getConnection() + ->getConfiguration(); + + $configuration->setSQLLogger($logger); + } + /** * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::countObjects() */ @@ -50,12 +84,13 @@ class Provider extends AbstractProvider */ $orderBy = $queryBuilder->getDQLPart('orderBy'); if (empty($orderBy)) { + $rootAliases = $queryBuilder->getRootAliases(); $identifierFieldNames = $this->managerRegistry ->getManagerForClass($this->objectClass) ->getClassMetadata($this->objectClass) ->getIdentifierFieldNames(); foreach ($identifierFieldNames as $fieldName) { - $queryBuilder->addOrderBy(static::ENTITY_ALIAS.'.'.$fieldName); + $queryBuilder->addOrderBy($rootAliases[0].'.'.$fieldName); } } diff --git a/DynamicIndex.php b/DynamicIndex.php index cbec8e9..484a0d6 100644 --- a/DynamicIndex.php +++ b/DynamicIndex.php @@ -2,27 +2,11 @@ namespace FOS\ElasticaBundle; -use Elastica\Index; +use FOS\ElasticaBundle\Elastica\Index; /** - * Elastica index capable of reassigning name dynamically - * - * @author Konstantin Tjuterev + * @deprecated Use \FOS\ElasticaBundle\Elastica\TransformingIndex */ class DynamicIndex extends Index { - /** - * 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) - { - $this->_name = $name; - } } diff --git a/Elastica/Client.php b/Elastica/Client.php new file mode 100644 index 0000000..372b395 --- /dev/null +++ b/Elastica/Client.php @@ -0,0 +1,103 @@ + + */ +class Client extends BaseClient +{ + /** + * Stores created indexes to avoid recreation. + * + * @var array + */ + private $indexCache = array(); + + /** + * Symfony's debugging Stopwatch + * + * @var Stopwatch|null + */ + private $stopwatch; + + /** + * @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()) + { + if ($this->stopwatch) { + $this->stopwatch->start('es_request', 'fos_elastica'); + } + + $start = microtime(true); + $response = parent::request($path, $method, $data, $query); + + $this->logQuery($path, $method, $data, $query, $start); + + if ($this->stopwatch) { + $this->stopwatch->stop('es_request'); + } + + return $response; + } + + public function getIndex($name) + { + if (isset($this->indexCache[$name])) { + return $this->indexCache[$name]; + } + + return $this->indexCache[$name] = new Index($this, $name); + } + + /** + * Sets a stopwatch instance for debugging purposes. + * + * @param Stopwatch $stopwatch + */ + public function setStopwatch(Stopwatch $stopwatch = null) + { + $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 new file mode 100644 index 0000000..49c656e --- /dev/null +++ b/Elastica/Index.php @@ -0,0 +1,61 @@ + + */ +class Index extends BaseIndex +{ + private $originalName; + + /** + * Stores created types to avoid recreation. + * + * @var array + */ + private $typeCache = array(); + + /** + * Returns the original name of the index if the index has been renamed for reindexing + * or realiasing purposes. + * + * @return string + */ + public function getOriginalName() + { + return $this->originalName ?: $this->_name; + } + + /** + * @param string $type + */ + public function getType($type) + { + if (isset($this->typeCache[$type])) { + return $this->typeCache[$type]; + } + + return $this->typeCache[$type] = parent::getType($type); + } + + /** + * 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) + { + $this->originalName = $this->_name; + $this->_name = $name; + } +} diff --git a/Exception/AliasIsIndexException.php b/Exception/AliasIsIndexException.php new file mode 100644 index 0000000..87f546b --- /dev/null +++ b/Exception/AliasIsIndexException.php @@ -0,0 +1,12 @@ +addCompilerPass(new ConfigSourcePass()); + $container->addCompilerPass(new IndexPass()); $container->addCompilerPass(new RegisterProvidersPass(), PassConfig::TYPE_BEFORE_REMOVING); $container->addCompilerPass(new TransformerPass()); } diff --git a/Index/AliasProcessor.php b/Index/AliasProcessor.php new file mode 100644 index 0000000..40e61b8 --- /dev/null +++ b/Index/AliasProcessor.php @@ -0,0 +1,178 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Index; + +use Elastica\Client; +use Elastica\Exception\ExceptionInterface; +use Elastica\Request; +use FOS\ElasticaBundle\Configuration\IndexConfig; +use FOS\ElasticaBundle\Elastica\Index; +use FOS\ElasticaBundle\Exception\AliasIsIndexException; + +class AliasProcessor +{ + /** + * Sets the randomised root name for an index. + * + * @param IndexConfig $indexConfig + * @param Index $index + */ + public function setRootName(IndexConfig $indexConfig, Index $index) + { + $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 bool $force + * @throws AliasIsIndexException + * @throws \RuntimeException + */ + public function switchIndexAlias(IndexConfig $indexConfig, Index $index, $force = false) + { + $client = $index->getClient(); + + $aliasName = $indexConfig->getElasticSearchName(); + $oldIndexName = false; + $newIndexName = $index->getName(); + + try { + $aliasedIndexes = $this->getAliasedIndexes($client, $aliasName); + } catch(AliasIsIndexException $e) { + if (!$force) { + throw $e; + } + + $this->deleteIndex($client, $aliasName); + $aliasedIndexes = array(); + } + + 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) + ) + ); + } + + $aliasUpdateRequest = array('actions' => array()); + if (count($aliasedIndexes) === 1) { + // if the alias is set - add an action to remove it + $oldIndexName = $aliasedIndexes[0]; + $aliasUpdateRequest['actions'][] = array( + 'remove' => array('index' => $oldIndexName, 'alias' => $aliasName) + ); + } + + // add an action to point the alias to the new index + $aliasUpdateRequest['actions'][] = array( + '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() + ); + } + + throw new \RuntimeException( + sprintf( + 'Failed to updated index alias: %s. %s', + $renameAliasException->getMessage(), + $additionalError ?: sprintf('Newly built index %s was deleted', $newIndexName) + ), 0, $renameAliasException + ); + } + + // 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 + ); + } + } + } + + /** + * Returns array of indexes which are mapped to given alias + * + * @param Client $client + * @param string $aliasName Alias name + * + * @return array + * @throws AliasIsIndexException + */ + private function getAliasedIndexes(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; + } + + /** + * Delete an index + * + * @param Client $client + * @param string $indexName Index name to delete + */ + private function deleteIndex(Client $client, $indexName) + { + $path = sprintf("%s", $indexName); + $client->request($path, Request::DELETE); + } +} diff --git a/Index/IndexManager.php b/Index/IndexManager.php new file mode 100644 index 0000000..38249a7 --- /dev/null +++ b/Index/IndexManager.php @@ -0,0 +1,63 @@ +defaultIndex = $defaultIndex; + $this->indexes = $indexes; + } + + /** + * Gets all registered indexes + * + * @return array + */ + public function getAllIndexes() + { + return $this->indexes; + } + + /** + * 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) + { + if (null === $name) { + return $this->defaultIndex; + } + + if (!isset($this->indexes[$name])) { + throw new \InvalidArgumentException(sprintf('The index "%s" does not exist', $name)); + } + + return $this->indexes[$name]; + } + + /** + * Gets the default index + * + * @return Index + */ + public function getDefaultIndex() + { + return $this->defaultIndex; + } +} diff --git a/Index/MappingBuilder.php b/Index/MappingBuilder.php new file mode 100644 index 0000000..21ae871 --- /dev/null +++ b/Index/MappingBuilder.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Index; + +use FOS\ElasticaBundle\Configuration\IndexConfig; +use FOS\ElasticaBundle\Configuration\TypeConfig; + +class MappingBuilder +{ + /** + * Skip adding default information to certain fields. + * + * @var array + */ + private $skipTypes = array('completion'); + + /** + * Builds mappings for an entire index. + * + * @param IndexConfig $indexConfig + * @return array + */ + public function buildIndexMapping(IndexConfig $indexConfig) + { + $typeMappings = array(); + foreach ($indexConfig->getTypes() as $typeConfig) { + $typeMappings[$typeConfig->getName()] = $this->buildTypeMapping($typeConfig); + } + + $mapping = array(); + if ($typeMappings) { + $mapping['mappings'] = $typeMappings; + } + // 'warmers' => $indexConfig->getWarmers(), + + $settings = $indexConfig->getSettings(); + if ($settings) { + $mapping['settings'] = $settings; + } + + return $mapping; + } + + /** + * 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(), + )); + + if ($typeConfig->getIndexAnalyzer()) { + $mapping['index_analyzer'] = $typeConfig->getIndexAnalyzer(); + } + + if ($typeConfig->getSearchAnalyzer()) { + $mapping['search_analyzer'] = $typeConfig->getSearchAnalyzer(); + } + + if (isset($mapping['dynamic_templates']) and empty($mapping['dynamic_templates'])) { + unset($mapping['dynamic_templates']); + } + + $this->fixProperties($mapping['properties']); + if (!$mapping['properties']) { + unset($mapping['properties']); + } + + if ($typeConfig->getModel()) { + $mapping['_meta']['model'] = $typeConfig->getModel(); + } + + if (!$mapping) { + // Empty mapping, we want it encoded as a {} instead of a [] + $mapping = new \stdClass; + } + + return $mapping; + } + + /** + * Fixes any properties and applies basic defaults for any field that does not have + * required options. + * + * @param $properties + */ + private function fixProperties(&$properties) + { + foreach ($properties as $name => &$property) { + if (!isset($property['type'])) { + $property['type'] = 'string'; + } + if (isset($property['properties'])) { + $this->fixProperties($property['properties']); + } + if (in_array($property['type'], $this->skipTypes)) { + continue; + } + if (!isset($property['store'])) { + $property['store'] = true; + } + } + } +} diff --git a/Index/Resetter.php b/Index/Resetter.php new file mode 100644 index 0000000..9b65a8f --- /dev/null +++ b/Index/Resetter.php @@ -0,0 +1,122 @@ +aliasProcessor = $aliasProcessor; + $this->configManager = $configManager; + $this->indexManager = $indexManager; + $this->mappingBuilder = $mappingBuilder; + } + + /** + * Deletes and recreates all indexes + */ + public function resetAllIndexes($populating = false, $force = false) + { + foreach ($this->configManager->getIndexNames() as $name) { + $this->resetIndex($name, $populating, $force); + } + } + + /** + * Deletes and recreates the named index. If populating, creates a new index + * with a randomised name for an alias to be set after population. + * + * @param string $indexName + * @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, $force = false) + { + $indexConfig = $this->configManager->getIndexConfiguration($indexName); + $index = $this->indexManager->getIndex($indexName); + + if ($indexConfig->isUseAlias()) { + $this->aliasProcessor->setRootName($indexConfig, $index); + } + + $mapping = $this->mappingBuilder->buildIndexMapping($indexConfig); + $index->create($mapping, true); + + if (!$populating and $indexConfig->isUseAlias()) { + $this->aliasProcessor->switchIndexAlias($indexConfig, $index, $force); + } + } + + /** + * 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 + */ + public function resetIndexType($indexName, $typeName) + { + $typeConfig = $this->configManager->getTypeConfiguration($indexName, $typeName); + $type = $this->indexManager->getIndex($indexName)->getType($typeName); + + try { + $type->delete(); + } catch (ResponseException $e) { + if (strpos($e->getMessage(), 'TypeMissingException') === false) { + throw $e; + } + } + + $mapping = new Mapping; + foreach ($this->mappingBuilder->buildTypeMapping($typeConfig) as $name => $field) { + $mapping->setParam($name, $field); + } + + $type->setMapping($mapping); + } + + /** + * A command run when a population has finished. + * + * @param string $indexName + */ + public function postPopulate($indexName) + { + $indexConfig = $this->configManager->getIndexConfiguration($indexName); + + if ($indexConfig->isUseAlias()) { + $index = $this->indexManager->getIndex($indexName); + $this->aliasProcessor->switchIndexAlias($indexConfig, $index); + } + } +} diff --git a/IndexManager.php b/IndexManager.php index e20a791..e7c74c8 100644 --- a/IndexManager.php +++ b/IndexManager.php @@ -2,62 +2,11 @@ namespace FOS\ElasticaBundle; -use Elastica\Index; +use FOS\ElasticaBundle\Index\IndexManager as BaseIndexManager; -class IndexManager +/** + * @deprecated Use \FOS\ElasticaBundle\Index\IndexManager + */ +class IndexManager extends BaseIndexManager { - protected $indexesByName; - protected $defaultIndexName; - - /** - * Constructor. - * - * @param array $indexesByName - * @param Index $defaultIndex - */ - public function __construct(array $indexesByName, Index $defaultIndex) - { - $this->indexesByName = $indexesByName; - $this->defaultIndexName = $defaultIndex->getName(); - } - - /** - * Gets all registered indexes - * - * @return array - */ - public function getAllIndexes() - { - return $this->indexesByName; - } - - /** - * 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) - { - if (null === $name) { - $name = $this->defaultIndexName; - } - - if (!isset($this->indexesByName[$name])) { - throw new \InvalidArgumentException(sprintf('The index "%s" does not exist', $name)); - } - - return $this->indexesByName[$name]; - } - - /** - * Gets the default index - * - * @return Index - */ - public function getDefaultIndex() - { - return $this->getIndex($this->defaultIndexName); - } } diff --git a/Manager/RepositoryManager.php b/Manager/RepositoryManager.php index 3cf8e96..7697b58 100644 --- a/Manager/RepositoryManager.php +++ b/Manager/RepositoryManager.php @@ -59,7 +59,7 @@ class RepositoryManager implements RepositoryManagerInterface } $refClass = new \ReflectionClass($entityName); - $annotation = $this->reader->getClassAnnotation($refClass, 'FOS\\ElasticaBundle\\Configuration\\Search'); + $annotation = $this->reader->getClassAnnotation($refClass, 'FOS\\ElasticaBundle\\Annotation\\Search'); if ($annotation) { $this->entities[$entityName]['repositoryName'] = $annotation->repositoryClass; @@ -69,6 +69,9 @@ class RepositoryManager implements RepositoryManagerInterface return 'FOS\ElasticaBundle\Repository'; } + /** + * @param string $entityName + */ private function createRepository($entityName) { if (!class_exists($repositoryName = $this->getRepositoryName($entityName))) { diff --git a/Paginator/RawPaginatorAdapter.php b/Paginator/RawPaginatorAdapter.php index 62dc95e..7e2d49b 100644 --- a/Paginator/RawPaginatorAdapter.php +++ b/Paginator/RawPaginatorAdapter.php @@ -59,8 +59,8 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface /** * Returns the paginated results. * - * @param $offset - * @param $itemCountPerPage + * @param integer $offset + * @param integer $itemCountPerPage * @throws \InvalidArgumentException * @return ResultSet */ @@ -132,6 +132,7 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface return $this->facets; } +<<<<<<< HEAD /** * Returns Aggregations @@ -146,6 +147,8 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface return $this->aggregations; } +======= +>>>>>>> upstream/master /** * Returns the Query diff --git a/Persister/ObjectPersister.php b/Persister/ObjectPersister.php index 3592a78..0fe40c3 100644 --- a/Persister/ObjectPersister.php +++ b/Persister/ObjectPersister.php @@ -2,7 +2,8 @@ namespace FOS\ElasticaBundle\Persister; -use Elastica\Exception\NotFoundException; +use Psr\Log\LoggerInterface; +use Elastica\Exception\BulkException; use FOS\ElasticaBundle\Transformer\ModelToElasticaTransformerInterface; use Elastica\Type; use Elastica\Document; @@ -19,6 +20,7 @@ class ObjectPersister implements ObjectPersisterInterface protected $transformer; protected $objectClass; protected $fields; + protected $logger; public function __construct(Type $type, ModelToElasticaTransformerInterface $transformer, $objectClass, array $fields) { @@ -28,6 +30,38 @@ class ObjectPersister implements ObjectPersisterInterface $this->fields = $fields; } + /** + * If the ObjectPersister handles a given object. + * + * @param object $object + * @return bool + */ + public function handlesObject($object) + { + return $object instanceof $this->objectClass; + } + + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * 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) + { + if (! $this->logger) { + throw $e; + } + + $this->logger->error($e); + } + /** * Insert one object into the type * The object will be transformed to an elastica document @@ -36,8 +70,7 @@ class ObjectPersister implements ObjectPersisterInterface */ public function insertOne($object) { - $document = $this->transformToElasticaDocument($object); - $this->type->addDocument($document); + $this->insertMany(array($object)); } /** @@ -48,11 +81,7 @@ class ObjectPersister implements ObjectPersisterInterface **/ public function replaceOne($object) { - $document = $this->transformToElasticaDocument($object); - try { - $this->type->deleteById($document->getId()); - } catch (NotFoundException $e) {} - $this->type->addDocument($document); + $this->replaceMany(array($object)); } /** @@ -63,10 +92,7 @@ class ObjectPersister implements ObjectPersisterInterface **/ public function deleteOne($object) { - $document = $this->transformToElasticaDocument($object); - try { - $this->type->deleteById($document->getId()); - } catch (NotFoundException $e) {} + $this->deleteMany(array($object)); } /** @@ -78,9 +104,7 @@ class ObjectPersister implements ObjectPersisterInterface **/ public function deleteById($id) { - try { - $this->type->deleteById($id); - } catch (NotFoundException $e) {} + $this->deleteManyByIdentifiers(array($id)); } /** @@ -95,11 +119,15 @@ class ObjectPersister implements ObjectPersisterInterface foreach ($objects as $object) { $documents[] = $this->transformToElasticaDocument($object); } - $this->type->addDocuments($documents); + try { + $this->type->addDocuments($documents); + } catch (BulkException $e) { + $this->log($e); + } } /** - * Bulk updates an array of objects in the type + * Bulk update an array of objects in the type. Create document if it does not already exist. * * @param array $objects array of domain model objects */ @@ -107,9 +135,16 @@ class ObjectPersister implements ObjectPersisterInterface { $documents = array(); foreach ($objects as $object) { - $documents[] = $this->transformToElasticaDocument($object); + $document = $this->transformToElasticaDocument($object); + $document->setDocAsUpsert(true); + $documents[] = $document; + } + + try { + $this->type->updateDocuments($documents); + } catch (BulkException $e) { + $this->log($e); } - $this->type->updateDocuments($documents); } /** @@ -123,7 +158,25 @@ class ObjectPersister implements ObjectPersisterInterface foreach ($objects as $object) { $documents[] = $this->transformToElasticaDocument($object); } - $this->type->deleteDocuments($documents); + try { + $this->type->deleteDocuments($documents); + } catch (BulkException $e) { + $this->log($e); + } + } + + /** + * Bulk deletes records from an array of identifiers + * + * @param array $identifiers array of domain model object identifiers + */ + public function deleteManyByIdentifiers(array $identifiers) + { + try { + $this->type->getIndex()->getClient()->deleteIds($identifiers, $this->type->getIndex(), $this->type); + } catch (BulkException $e) { + $this->log($e); + } } /** @@ -136,4 +189,4 @@ class ObjectPersister implements ObjectPersisterInterface { return $this->transformer->transform($object, $this->fields); } -} \ No newline at end of file +} diff --git a/Persister/ObjectPersisterInterface.php b/Persister/ObjectPersisterInterface.php index a25aafc..0df7f7e 100644 --- a/Persister/ObjectPersisterInterface.php +++ b/Persister/ObjectPersisterInterface.php @@ -61,4 +61,19 @@ interface ObjectPersisterInterface * @param array $objects array of domain model objects */ function deleteMany(array $objects); + + /** + * Bulk deletes records from an array of identifiers + * + * @param array $identifiers array of domain model object identifiers + */ + public function deleteManyByIdentifiers(array $identifiers); + + /** + * If the object persister handles the given object. + * + * @param object $object + * @return bool + */ + public function handlesObject($object); } diff --git a/Persister/ObjectSerializerPersister.php b/Persister/ObjectSerializerPersister.php index 1a15656..3e33f8d 100644 --- a/Persister/ObjectSerializerPersister.php +++ b/Persister/ObjectSerializerPersister.php @@ -17,6 +17,9 @@ class ObjectSerializerPersister extends ObjectPersister { protected $serializer; + /** + * @param string $objectClass + */ public function __construct(Type $type, ModelToElasticaTransformerInterface $transformer, $objectClass, $serializer) { parent::__construct($type, $transformer, $objectClass, array()); diff --git a/Propel/ElasticaToModelTransformer.php b/Propel/ElasticaToModelTransformer.php index af5f8ab..e3602e5 100644 --- a/Propel/ElasticaToModelTransformer.php +++ b/Propel/ElasticaToModelTransformer.php @@ -170,6 +170,7 @@ 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 393beba..38f7a61 100644 --- a/Propel/Provider.php +++ b/Propel/Provider.php @@ -30,14 +30,23 @@ class Provider extends AbstractProvider $objects = $queryClass::create() ->limit($batchSize) ->offset($offset) - ->find(); + ->find() + ->getArrayCopy(); + if ($loggerClosure) { + $stepNbObjects = count($objects); + } + $objects = array_filter($objects, array($this, 'isObjectIndexable')); + if (!$objects) { + $loggerClosure('Entire batch was filtered away, skipping...'); - $this->objectPersister->insertMany($objects->getArrayCopy()); + continue; + } + + $this->objectPersister->insertMany($objects); usleep($sleep); if ($loggerClosure) { - $stepNbObjects = count($objects); $stepCount = $stepNbObjects + $offset; $percentComplete = 100 * $stepCount / $nbObjects; $objectsPerSecond = $stepNbObjects / (microtime(true) - $stepStartTime); diff --git a/Provider/AbstractProvider.php b/Provider/AbstractProvider.php index 2761a25..82ea914 100644 --- a/Provider/AbstractProvider.php +++ b/Provider/AbstractProvider.php @@ -24,23 +24,49 @@ abstract class AbstractProvider implements ProviderInterface */ protected $options; + /** + * @var Indexable + */ + private $indexable; + /** * Constructor. * * @param ObjectPersisterInterface $objectPersister - * @param string $objectClass - * @param array $options + * @param IndexableInterface $indexable + * @param string $objectClass + * @param array $options */ - public function __construct(ObjectPersisterInterface $objectPersister, $objectClass, array $options = array()) - { - $this->objectPersister = $objectPersister; + public function __construct( + ObjectPersisterInterface $objectPersister, + IndexableInterface $indexable, + $objectClass, + array $options = array() + ) { + $this->indexable = $indexable; $this->objectClass = $objectClass; + $this->objectPersister = $objectPersister; $this->options = array_merge(array( 'batch_size' => 100, ), $options); } + /** + * Checks if a given object should be indexed or not. + * + * @param object $object + * @return bool + */ + protected function isObjectIndexable($object) + { + return $this->indexable->isObjectIndexable( + $this->options['indexName'], + $this->options['typeName'], + $object + ); + } + /** * Get string with RAM usage information (current and peak) * diff --git a/Provider/Indexable.php b/Provider/Indexable.php new file mode 100644 index 0000000..197aeb8 --- /dev/null +++ b/Provider/Indexable.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Provider; + +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\ExpressionLanguage\SyntaxError; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +class Indexable implements IndexableInterface +{ + /** + * An array of raw configured callbacks for all types. + * + * @var array + */ + private $callbacks = array(); + + /** + * @var \Symfony\Component\DependencyInjection\ContainerInterface + */ + private $container; + + /** + * An instance of ExpressionLanguage + * + * @var ExpressionLanguage + */ + private $expressionLanguage; + + /** + * An array of initialised callbacks. + * + * @var array + */ + private $initialisedCallbacks = array(); + + /** + * PropertyAccessor instance + * + * @var PropertyAccessorInterface + */ + private $propertyAccessor; + + /** + * @param array $callbacks + */ + public function __construct(array $callbacks, ContainerInterface $container) + { + $this->callbacks = $callbacks; + $this->container = $container; + $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); + } + + /** + * Return whether the object is indexable with respect to the callback. + * + * @param string $indexName + * @param string $typeName + * @param mixed $object + * @return bool + */ + public function isObjectIndexable($indexName, $typeName, $object) + { + $type = sprintf('%s/%s', $indexName, $typeName); + $callback = $this->getCallback($type, $object); + if (!$callback) { + return true; + } + + if ($callback instanceof Expression) { + return $this->getExpressionLanguage()->evaluate($callback, array( + 'object' => $object, + $this->getExpressionVar($object) => $object + )); + } + + return is_string($callback) + ? call_user_func(array($object, $callback)) + : call_user_func($callback, $object); + } + + /** + * Builds and initialises a callback. + * + * @param string $type + * @param object $object + * @return mixed + */ + private function buildCallback($type, $object) + { + if (!array_key_exists($type, $this->callbacks)) { + return null; + } + + $callback = $this->callbacks[$type]; + + if (is_callable($callback) or is_callable(array($object, $callback))) { + 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_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); + } + } + + throw new \InvalidArgumentException(sprintf('Callback for type "%s" is not a valid callback.', $type)); + } + + /** + * 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) + { + if (!array_key_exists($type, $this->initialisedCallbacks)) { + $this->initialisedCallbacks[$type] = $this->buildCallback($type, $object); + } + + return $this->initialisedCallbacks[$type]; + } + + /** + * @return bool|ExpressionLanguage + */ + private function getExpressionLanguage() + { + if (null === $this->expressionLanguage) { + if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + return false; + } + + $this->expressionLanguage = new ExpressionLanguage(); + } + + return $this->expressionLanguage; + } + + /** + * @param mixed $object + * @return string + */ + private function getExpressionVar($object = null) + { + $ref = new \ReflectionClass($object); + + return strtolower($ref->getShortName()); + } +} diff --git a/Provider/IndexableInterface.php b/Provider/IndexableInterface.php new file mode 100644 index 0000000..4871b58 --- /dev/null +++ b/Provider/IndexableInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Provider; + +interface IndexableInterface +{ + /** + * Checks if an object passed should be indexable or not. + * + * @param string $indexName + * @param string $typeName + * @param mixed $object + * @return bool + */ + public function isObjectIndexable($indexName, $typeName, $object); +} diff --git a/README.md b/README.md index 112ec69..01afa6d 100644 --- a/README.md +++ b/README.md @@ -1,897 +1,35 @@ -[Elastica](https://github.com/ruflin/Elastica) integration in Symfony2 +FOSElasticaBundle +================= -### Installation +This bundle provides integration with [ElasticSearch](http://www.elasticsearch.org) and [Elastica](https://github.com/ruflin/Elastica) with +Symfony2. Features include: -#### Bundle and Dependencies +- Integrates the Elastica library into a Symfony2 environment +- Automatically generate mappings using a serializer +- Listeners for Doctrine events for automatic indexing -For Symfony 2.0.x projects, you must use a 1.x release of this bundle. Please -check the bundle -[tags](https://github.com/FriendsOfSymfony/FOSElasticaBundle/tags) or the -[Packagist](https://packagist.org/packages/friendsofsymfony/elastica-bundle) -page for information on Symfony and Elastica compatibility. +> **Note** Propel support is limited and contributions fixing issues are welcome! -Add FOSElasticaBundle to your application's `composer.json` file: +[![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) -```json -{ - "require": { - "friendsofsymfony/elastica-bundle": "3.0.*@dev" - } -} -``` +Documentation +------------- -Install the bundle and its dependencies with the following command: +Documentation for FOSElasticaBundle is in `Resources/doc/index.md` -```bash -$ php composer.phar update friendsofsymfony/elastica-bundle -``` +[Read the documentation for 3.0.x (master)](https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/master/Resources/doc/index.md) -You may rely on Composer to fetch the appropriate version of Elastica. Lastly, -enable the bundle in your application kernel: +[Read the documentation for 2.1.x](https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/2.1.x/README.md) -```php -// app/AppKernel.php +Installation +------------ -public function registerBundles() -{ - $bundles = array( - // ... - new FOS\ElasticaBundle\FOSElasticaBundle(), - ); -} -``` +Installation instructions can be found in the [documentation](https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/master/Resources/doc/setup.md) -#### Elasticsearch +License +------- -Instructions for installing and deploying Elasticsearch may be found -[here](http://www.elasticsearch.org/guide/reference/setup/installation/). +This bundle is under the MIT license. See the complete license in the bundle: -### Basic configuration - -#### Declare a client - -Elasticsearch client is comparable to a database connection. -Most of the time, you will need only one. - - #app/config/config.yml - fos_elastica: - clients: - default: { host: localhost, port: 9200 } - -If your client requires Basic HTTP Authentication, you can specify an Authorization Header to -include in HTTP requests. The Authorization Header value is a ``base64`` encoded string that -includes the authentication username and password, and can be obtained by running the following -command in your terminal: - - php -r "Print 'Basic ' . base64_encode('your_auth_username' . ':' . 'your_auth_password');" - -A sample configuration with Basic HTTP Authentication is: - - #app/config/config.yml - fos_elastica: - clients: - default: - host: example.com - port: 80 - headers: - Authorization: "Basic jdumrGK7rY9TMuQOPng7GZycmxyMHNoir==" - -A client configuration can also override the Elastica logger to change the used class ```logger: ``` or to simply disable it ```logger: false```. Disabling the logger should be done on production because it can cause a memory leak. - -#### Declare a serializer - -Elastica can handle objects instead of data arrays if a serializer callable is configured - - #app/config/config.yml - fos_elastica: - clients: - default: { host: localhost, port: 9200 } - serializer: - callback_class: callback_class - serializer: serializer - -``callback_class`` is the name of a class having a public method serialize($object) and should -extends from ``FOS\ElasticaBundle\Serializer\Callback``. - -``serializer`` is the service id for the actual serializer, e.g. ``serializer`` if you're using -JMSSerializerBundle. If this is configured you can use ``\Elastica\Type::addObject`` instead of -``\Elastica\Type::addDocument`` to add data to the index. The bundle provides a default implementation -with a serializer service id 'serializer' that can be turned on by adding the following line to your config. - - #app/config/config.yml - fos_elastica: - serializer: ~ - -#### Declare an index - -Elasticsearch index is comparable to Doctrine entity manager. -Most of the time, you will need only one. - - fos_elastica: - clients: - default: { host: localhost, port: 9200 } - serializer: - callback_class: FOS\ElasticaBundle\Serializer\Callback - serializer: serializer - indexes: - website: - client: default - -Here we created a "website" index, that uses our "default" client. - -Our index is now available as a service: `fos_elastica.index.website`. It is an instance of `\Elastica\Index`. - -If you need to have different index name from the service name, for example, -in order to have different indexes for different environments then you can -use the ```index_name``` key to change the index name. The service name will -remain the same across the environments: - - fos_elastica: - clients: - default: { host: localhost, port: 9200 } - indexes: - website: - client: default - index_name: website_qa - -The service id will be `fos_elastica.index.website` but the underlying index name is website_qa. - -#### Declare a type - -Elasticsearch type is comparable to Doctrine entity repository. - - fos_elastica: - clients: - default: { host: localhost, port: 9200 } - serializer: - callback_class: FOS\ElasticaBundle\Serializer\Callback - serializer: serializer - indexes: - website: - client: default - types: - user: - mappings: - username: { boost: 5 } - firstName: { boost: 3 } - lastName: { boost: 3 } - aboutMe: ~ - -Our type is now available as a service: `fos_elastica.index.website.user`. It is an instance of `\Elastica\Type`. - -### Declaring serializer groups - -If you are using the JMSSerializerBundle for serializing objects passed to elastica you can define serializer groups -per type. - - fos_elastica: - clients: - default: { host: localhost, port: 9200 } - serializer: - callback_class: %classname% - serializer: serializer - indexes: - website: - client: default - types: - user: - mappings: - username: { boost: 5 } - firstName: { boost: 3 } - lastName: { boost: 3 } - aboutMe: - serializer: - groups: [elastica, Default] - -### Declaring parent field - - fos_elastica: - clients: - default: { host: localhost, port: 9200 } - serializer: - callback_class: FOS\ElasticaBundle\Serializer\Callback - serializer: serializer - indexes: - website: - client: default - types: - comment: - mappings: - date: { boost: 5 } - content: ~ - _parent: { type: "post", property: "post", identifier: "id" } - -The parent field declaration has the following values: - - * `type`: The parent type. - * `property`: The property in the child entity where to look for the parent entity. It may be ignored if is equal to the parent type. - * `identifier`: The property in the parent entity which has the parent identifier. Defaults to `id`. - -Note that to create a document with a parent, you need to call `setParent` on the document rather than setting a _parent field. -If you do this wrong, you will see a `RoutingMissingException` as elasticsearch does not know where to store a document that should have a parent but does not specify it. - -### Declaring `nested` or `object` - -Note that object can autodetect properties - - fos_elastica: - clients: - default: { host: localhost, port: 9200 } - serializer: - callback_class: FOS\ElasticaBundle\Serializer\Callback - serializer: serializer - indexes: - website: - client: default - types: - post: - mappings: - date: { boost: 5 } - title: { boost: 3 } - content: ~ - comments: - type: "nested" - properties: - date: { boost: 5 } - content: ~ - user: - type: "object" - approver: - type: "object" - properties: - date: { boost: 5 } - -#### Doctrine ORM and `object` mappings - -Objects operate in the same way as the nested results but they need to have associations set up in Doctrine ORM so that they can be referenced correctly when indexing. - -If an "Entity was not found" error occurs while indexing, a null association has been discovered in the database. A custom Doctrine query must be used to utilize left joins instead of the default inner join. - -### Populate the types - - php app/console fos:elastica:populate - -This command deletes and creates the declared indexes and types. -It applies the configured mappings to the types. - -This command needs providers to insert new documents in the elasticsearch types. -There are 2 ways to create providers. -If your elasticsearch type matches a Doctrine repository or a Propel query, go for the persistence automatic provider. -Or, for complete flexibility, go for a manual provider. - -#### Persistence automatic provider - -If we want to index the entities from a Doctrine repository or a Propel query, -some configuration will let ElasticaBundle do it for us. - - fos_elastica: - clients: - default: { host: localhost, port: 9200 } - serializer: - callback_class: FOS\ElasticaBundle\Serializer\Callback - serializer: serializer - indexes: - website: - client: default - types: - user: - mappings: - username: { boost: 5 } - firstName: { boost: 3 } - # more mappings... - persistence: - driver: orm # orm, mongodb, propel are available - model: Application\UserBundle\Entity\User - provider: ~ - -Three drivers are actually supported: orm, mongodb, and propel. - -##### Use a custom Doctrine query builder - -You can control which entities will be indexed by specifying a custom query builder method. - - persistence: - driver: orm - model: Application\UserBundle\Entity\User - provider: - query_builder_method: createIsActiveQueryBuilder - -Your repository must implement this method and return a Doctrine query builder. - -> **Propel** doesn't support this feature yet. - -##### Change the batch size - -By default, ElasticaBundle will index documents by packets of 100. -You can change this value in the provider configuration. - - persistence: - driver: orm - model: Application\UserBundle\Entity\User - provider: - batch_size: 100 - -##### Change the document identifier field - -By default, ElasticaBundle will use the `id` field of your entities as the elasticsearch document identifier. -You can change this value in the persistence configuration. - - persistence: - driver: orm - model: Application\UserBundle\Entity\User - identifier: id - -#### Manual provider - -Create a service with the tag "fos_elastica.provider" and attributes for the -index and type for which the service will provide. - - - - - - -Its class must implement `FOS\ElasticaBundle\Provider\ProviderInterface`. - - userType = $userType; - } - - /** - * Insert the repository objects in the type index - * - * @param \Closure $loggerClosure - * @param array $options - */ - public function populate(\Closure $loggerClosure = null, array $options = array()) - { - if ($loggerClosure) { - $loggerClosure('Indexing users'); - } - - $document = new Document(); - $document->setData(array('username' => 'Bob')); - $this->userType->addDocuments(array($document)); - } - } - -You will find a more complete implementation example in `src/FOS/ElasticaBundle/Doctrine/AbstractProvider.php`. - -### Search - -You can just use the index and type Elastica objects, provided as services, to perform searches. - - /** var Elastica\Type */ - $userType = $this->container->get('fos_elastica.index.website.user'); - - /** var Elastica\ResultSet */ - $resultSet = $userType->search('bob'); - -#### Doctrine/Propel finder - -If your elasticsearch type is bound to a Doctrine entity repository or a Propel query, -you can get your entities instead of Elastica results when you perform a search. -Declare that you want a Doctrine/Propel finder in your configuration: - - fos_elastica: - clients: - default: { host: localhost, port: 9200 } - serializer: - callback_class: FOS\ElasticaBundle\Serializer\Callback - serializer: serializer - indexes: - website: - client: default - types: - user: - mappings: - # your mappings - persistence: - driver: orm - model: Application\UserBundle\Entity\User - provider: ~ - finder: ~ - -You can now use the `fos_elastica.finder.website.user` service: - - /** var FOS\ElasticaBundle\Finder\TransformedFinder */ - $finder = $container->get('fos_elastica.finder.website.user'); - - /** var array of Acme\UserBundle\Entity\User */ - $users = $finder->find('bob'); - - /** var array of Acme\UserBundle\Entity\User limited to 10 results */ - $users = $finder->find('bob', 10); - -You can even get paginated results! - -Pagerfanta: - - /** var Pagerfanta\Pagerfanta */ - $userPaginator = $finder->findPaginated('bob'); - - /** Number of results to be used for paging the results */ - $countOfResults = $userPaginator->getNbResults(); - -Knp paginator: - - $paginator = $this->get('knp_paginator'); - $userPaginator = $paginator->paginate($finder->createPaginatorAdapter('bob')); - -You can also get both the Elastica results and the entities together from the finder. -You can then access the score, highlights etc. from the Elastica\Result whilst -still also getting the entity. - - /** var array of FOS\ElasticaBundle\HybridResult */ - $hybridResults = $finder->findHybrid('bob'); - foreach ($hybridResults as $hybridResult) { - - /** var Acme\UserBundle\Entity\User */ - $user = $hybridResult->getTransformed(); - - /** var Elastica\Result */ - $result = $hybridResult->getResult(); - } - -If you would like to access facets while using Pagerfanta they can be accessed through -the Adapter seen in the example below. - -```php -$query = new \Elastica\Query(); -$facet = new \Elastica\Facet\Terms('tags'); -$facet->setField('companyGroup'); -$query->addFacet($facet); - -$companies = $finder->findPaginated($query); -$companies->setMaxPerPage($params['limit']); -$companies->setCurrentPage($params['page']); - -$facets = $companies->getAdapter()->getFacets()); -``` - -##### Index wide finder - -You can also define a finder that will work on the entire index. Adjust your index -configuration as per below: - - fos_elastica: - indexes: - website: - client: default - finder: ~ - -You can now use the index wide finder service `fos_elastica.finder.website`: - - /** var FOS\ElasticaBundle\Finder\MappedFinder */ - $finder = $container->get('fos_elastica.finder.website'); - - // Returns a mixed array of any objects mapped - $results = $finder->find('bob'); - -#### Repositories - -As well as using the finder service for a particular Doctrine/Propel entity you -can use a manager service for each driver and get a repository for an entity to search -against. This allows you to use the same service rather than the particular finder. For -example: - - /** var FOS\ElasticaBundle\Manager\RepositoryManager */ - $repositoryManager = $container->get('fos_elastica.manager.orm'); - - /** var FOS\ElasticaBundle\Repository */ - $repository = $repositoryManager->getRepository('UserBundle:User'); - - /** var array of Acme\UserBundle\Entity\User */ - $users = $repository->find('bob'); - -You can also specify the full name of the entity instead of the shortcut syntax: - - /** var FOS\ElasticaBundle\Repository */ - $repository = $repositoryManager->getRepository('Application\UserBundle\Entity\User'); - -> The **2.0** branch doesn't support using `UserBundle:User` style syntax and you must use the full name of the entity. . - -##### Default Manager - -If you are only using one driver then its manager service is automatically aliased -to `fos_elastica.manager`. So the above example could be simplified to: - - /** var FOS\ElasticaBundle\Manager\RepositoryManager */ - $repositoryManager = $container->get('fos_elastica.manager'); - - /** var FOS\ElasticaBundle\Repository */ - $repository = $repositoryManager->getRepository('UserBundle:User'); - - /** var array of Acme\UserBundle\Entity\User */ - $users = $repository->find('bob'); - -If you use multiple drivers then you can choose which one is aliased to `fos_elastica.manager` -using the `default_manager` parameter: - - fos_elastica: - default_manager: mongodb #defaults to orm - clients: - default: { host: localhost, port: 9200 } - #-- - -##### Custom Repositories - -As well as the default repository you can create a custom repository for an entity and add -methods for particular searches. These need to extend `FOS\ElasticaBundle\Repository` to have -access to the finder: - -``` -find($query); - } -} -``` - -To use the custom repository specify it in the mapping for the entity: - - fos_elastica: - clients: - default: { host: localhost, port: 9200 } - indexes: - website: - client: default - types: - user: - mappings: - # your mappings - persistence: - driver: orm - model: Application\UserBundle\Entity\User - provider: ~ - finder: ~ - repository: Acme\ElasticaBundle\SearchRepository\UserRepository - -Then the custom queries will be available when using the repository returned from the manager: - - /** var FOS\ElasticaBundle\Manager\RepositoryManager */ - $repositoryManager = $container->get('fos_elastica.manager'); - - /** var FOS\ElasticaBundle\Repository */ - $repository = $repositoryManager->getRepository('UserBundle:User'); - - /** var array of Acme\UserBundle\Entity\User */ - $users = $repository->findWithCustomQuery('bob'); - -Alternatively you can specify the custom repository using an annotation in the entity: - -``` - **Propel** doesn't support this feature yet. - -Declare that you want to update the index in real time: - - fos_elastica: - clients: - default: { host: localhost, port: 9200 } - indexes: - website: - client: default - types: - user: - mappings: - # your mappings - persistence: - driver: orm - model: Application\UserBundle\Entity\User - listener: ~ # by default, listens to "insert", "update" and "delete" and updates `postFlush` - -Now the index is automatically updated each time the state of the bound Doctrine repository changes. -No need to repopulate the whole "user" index when a new `User` is created. - -You can also choose to only listen for some of the events: - - persistence: - listener: - insert: true - update: false - delete: true - -By default, the ElasticSearch index will be updated after flush. To update before flushing, set `immediate` -to `true`: - - persistence: - listener: - insert: true - update: false - delete: true - immediate: true - -> Using `immediate` to update ElasticSearch before flush completes may cause the ElasticSearch index to fall out of -> sync with the source database in the event of a crash during the flush itself, such as in the case of a bad query. - - -### Checking an entity method for listener - -If you use listeners to update your index, you may need to validate your -entities before you index them (e.g. only index "public" entities). Typically, -you'll want the listener to be consistent with the provider's query criteria. -This may be achieved by using the `is_indexable_callback` config parameter: - - persistence: - listener: - is_indexable_callback: "isPublic" - -If `is_indexable_callback` is a string and the entity has a method with the -specified name, the listener will only index entities for which the method -returns `true`. Additionally, you may provide a service and method name pair: - - persistence: - listener: - is_indexable_callback: [ "%custom_service_id%", "isIndexable" ] - -In this case, the callback_class will be the `isIndexable()` method on the specified -service and the object being considered for indexing will be passed as the only -argument. This allows you to do more complex validation (e.g. ACL checks). - -If you have the [Symfony ExpressionLanguage](https://github.com/symfony/expression-language) component installed, you can use expressions -to evaluate the callback: - - persistence: - listener: - is_indexable_callback: "user.isActive() && user.hasRole('ROLE_USER')" - -As you might expect, new entities will only be indexed if the callback_class returns -`true`. Additionally, modified entities will be updated or removed from the -index depending on whether the callback_class returns `true` or `false`, respectively. -The delete listener disregards the callback_class. - -> **Propel** doesn't support this feature yet. - -### Ignoring missing index results - -By default, FOSElasticaBundle will throw an exception if the results returned from -Elasticsearch are different from the results it finds from the chosen persistence -provider. This may pose problems for a large index where updates do not occur instantly -or another process has removed the results from your persistence provider without -updating Elasticsearch. - -The error you're likely to see is something like: -'Cannot find corresponding Doctrine objects for all Elastica results.' - -To solve this issue, each mapped object can be configured to ignore the missing results: - - persistence: - elastica_to_model_transformer: - ignore_missing: true - -### Advanced elasticsearch configuration - -Any setting can be specified when declaring a type. For example, to enable a custom analyzer, you could write: - - fos_elastica: - indexes: - doc: - settings: - index: - analysis: - analyzer: - my_analyzer: - type: custom - tokenizer: lowercase - filter : [my_ngram] - filter: - my_ngram: - type: "nGram" - min_gram: 3 - max_gram: 5 - types: - blog: - mappings: - title: { boost: 8, analyzer: my_analyzer } - -### Overriding the Client class to suppress exceptions - -By default, exceptions from the Elastica client library will propagate through -the bundle's Client class. For instance, if the elasticsearch server is offline, -issuing a request will result in an `Elastica\Exception\Connection` being thrown. -Depending on your needs, it may be desirable to suppress these exceptions and -allow searches to fail silently. - -One way to achieve this is to override the `fos_elastica.client.class` service -container parameter with a custom class. In the following example, we override -the `Client::request()` method and return the equivalent of an empty search -response if an exception occurred. - -``` -container->get('fos_elastica.finder.website.article'); -$boolQuery = new \Elastica\Query\Bool(); - -$fieldQuery = new \Elastica\Query\Text(); -$fieldQuery->setFieldQuery('title', 'I am a title string'); -$fieldQuery->setFieldParam('title', 'analyzer', 'my_analyzer'); -$boolQuery->addShould($fieldQuery); - -$tagsQuery = new \Elastica\Query\Terms(); -$tagsQuery->setTerms('tags', array('tag1', 'tag2')); -$boolQuery->addShould($tagsQuery); - -$categoryQuery = new \Elastica\Query\Terms(); -$categoryQuery->setTerms('categoryIds', array('1', '2', '3')); -$boolQuery->addMust($categoryQuery); - -$data = $finder->find($boolQuery); -``` - -Configuration: - -```yaml -fos_elastica: - clients: - default: { host: localhost, port: 9200 } - indexes: - site: - settings: - index: - analysis: - analyzer: - my_analyzer: - type: snowball - language: English - types: - article: - mappings: - title: { boost: 10, analyzer: my_analyzer } - tags: - categoryIds: - persistence: - driver: orm - model: Acme\DemoBundle\Entity\Article - provider: - finder: -``` - -### Filtering Results and Executing a Default Query - -If may want to omit certain results from a query, filtering can be more -performant than a basic query because the filter results can be cached. In turn, -the query is run against only a subset of the results. A common use case for -filtering would be if your data has fields that indicate whether records are -"active" or "inactive". The following example illustrates how to issue such a -query with Elastica: - -```php -$query = new \Elastica\Query\QueryString($queryString); -$term = new \Elastica\Filter\Term(array('active' => true)); - -$filteredQuery = new \Elastica\Query\Filtered($query, $term); -$results = $this->container->get('fos_elastica.finder.index.type')->find($filteredQuery); -``` - -### Date format example - -If you want to specify a [date format](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-date-format.html): - -```yaml -fos_elastica: - clients: - default: { host: localhost, port: 9200 } - indexes: - site: - types: - user: - mappings: - username: { type: string } - lastlogin: { type: date, format: basic_date_time } - birthday: { type: date, format: "yyyy-MM-dd" } -``` - -#### Dynamic templates - -Dynamic templates allow to define mapping templates that will be -applied when dynamic introduction of fields / objects happens. - -[Documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-root-object-type.html#_dynamic_templates) - -```yaml -fos_elastica: - clients: - default: { host: localhost, port: 9200 } - indexes: - site: - types: - user: - dynamic_templates: - my_template_1: - match: apples_* - mapping: - type: float - my_template_2: - match: * - match_mapping_type: string - mapping: - type: string - index: not_analyzed - mappings: - username: { type: string } -``` + Resources/meta/LICENSE diff --git a/Repository.php b/Repository.php index 70b2a21..fcc2784 100644 --- a/Repository.php +++ b/Repository.php @@ -19,21 +19,43 @@ 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/Resetter.php b/Resetter.php index fe963d0..2067579 100644 --- a/Resetter.php +++ b/Resetter.php @@ -2,245 +2,11 @@ namespace FOS\ElasticaBundle; -use Elastica\Exception\ExceptionInterface; -use Elastica\Index; -use Elastica\Exception\ResponseException; -use Elastica\Type\Mapping; +use FOS\ElasticaBundle\Index\Resetter as BaseResetter; /** - * Deletes and recreates indexes + * @deprecated Use \FOS\ElasticaBundle\Index\Resetter */ -class Resetter +class Resetter extends BaseResetter { - protected $indexConfigsByName; - - /** - * Constructor. - * - * @param array $indexConfigsByName - */ - public function __construct(array $indexConfigsByName) - { - $this->indexConfigsByName = $indexConfigsByName; - } - - /** - * Deletes and recreates all indexes - */ - public function resetAllIndexes() - { - foreach (array_keys($this->indexConfigsByName) as $name) { - $this->resetIndex($name); - } - } - - /** - * Deletes and recreates the named index - * - * @param string $indexName - * @throws \InvalidArgumentException if no index exists for the given name - */ - public function resetIndex($indexName) - { - $indexConfig = $this->getIndexConfig($indexName); - $esIndex = $indexConfig['index']; - if (isset($indexConfig['use_alias']) && $indexConfig['use_alias']) { - $name = $indexConfig['name_or_alias']; - $name .= uniqid(); - $esIndex->overrideName($name); - $esIndex->create($indexConfig['config']); - - return; - } - - $esIndex->create($indexConfig['config'], true); - } - - /** - * 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 - */ - public function resetIndexType($indexName, $typeName) - { - $indexConfig = $this->getIndexConfig($indexName); - - if (!isset($indexConfig['config']['mappings'][$typeName]['properties'])) { - throw new \InvalidArgumentException(sprintf('The mapping for index "%s" and type "%s" does not exist.', $indexName, $typeName)); - } - - $type = $indexConfig['index']->getType($typeName); - try { - $type->delete(); - } catch (ResponseException $e) { - if (strpos($e->getMessage(), 'TypeMissingException') === false) { - throw $e; - } - } - $mapping = $this->createMapping($indexConfig['config']['mappings'][$typeName]); - $type->setMapping($mapping); - } - - /** - * create type mapping object - * - * @param array $indexConfig - * @return Mapping - */ - protected function createMapping($indexConfig) - { - $mapping = Mapping::create($indexConfig['properties']); - - $mappingSpecialFields = array('_uid', '_id', '_source', '_all', '_analyzer', '_boost', '_routing', '_index', '_size', '_timestamp', '_ttl', 'dynamic_templates'); - foreach ($mappingSpecialFields as $specialField) { - if (isset($indexConfig[$specialField])) { - $mapping->setParam($specialField, $indexConfig[$specialField]); - } - } - - if (isset($indexConfig['_parent'])) { - $mapping->setParam('_parent', array('type' => $indexConfig['_parent']['type'])); - } - - return $mapping; - } - - /** - * Gets an index config by its name - * - * @param string $indexName Index name - * - * @param $indexName - * @return array - * @throws \InvalidArgumentException if no index config exists for the given name - */ - protected function getIndexConfig($indexName) - { - if (!isset($this->indexConfigsByName[$indexName])) { - throw new \InvalidArgumentException(sprintf('The configuration for index "%s" does not exist.', $indexName)); - } - - return $this->indexConfigsByName[$indexName]; - } - - public function postPopulate($indexName) - { - $indexConfig = $this->getIndexConfig($indexName); - if (isset($indexConfig['use_alias']) && $indexConfig['use_alias']) { - $this->switchIndexAlias($indexName); - } - } - - /** - * Switches the alias for given index (by key) to the newly populated index - * and deletes the old index - * - * @param string $indexName Index name - * - * @throws \RuntimeException - */ - private function switchIndexAlias($indexName) - { - $indexConfig = $this->getIndexConfig($indexName); - $esIndex = $indexConfig['index']; - $aliasName = $indexConfig['name_or_alias']; - $oldIndexName = false; - $newIndexName = $esIndex->getName(); - - $aliasedIndexes = $this->getAliasedIndexes($esIndex, $aliasName); - - 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) - ) - ); - } - - // Change the alias to point to the new index - // Elastica's addAlias can't be used directly, because in current (0.19.x) version it's not atomic - // In 0.20.x it's atomic, but it doesn't return the old index name - $aliasUpdateRequest = array('actions' => array()); - if (count($aliasedIndexes) == 1) { - // if the alias is set - add an action to remove it - $oldIndexName = $aliasedIndexes[0]; - $aliasUpdateRequest['actions'][] = array( - 'remove' => array('index' => $oldIndexName, 'alias' => $aliasName) - ); - } - - // add an action to point the alias to the new index - $aliasUpdateRequest['actions'][] = array( - 'add' => array('index' => $newIndexName, 'alias' => $aliasName) - ); - - try { - $esIndex->getClient()->request('_aliases', 'POST', $aliasUpdateRequest); - } catch (ExceptionInterface $renameAliasException) { - $additionalError = ''; - // if we failed to move the alias, delete the newly built index - try { - $esIndex->delete(); - } catch (ExceptionInterface $deleteNewIndexException) { - $additionalError = sprintf( - 'Tried to delete newly built index %s, but also failed: %s', - $newIndexName, - $deleteNewIndexException->getError() - ); - } - - throw new \RuntimeException( - sprintf( - 'Failed to updated index alias: %s. %s', - $renameAliasException->getMessage(), - $additionalError ?: sprintf('Newly built index %s was deleted', $newIndexName) - ) - ); - } - - // Delete the old index after the alias has been switched - if ($oldIndexName) { - $oldIndex = new Index($esIndex->getClient(), $oldIndexName); - try { - $oldIndex->delete(); - } catch (ExceptionInterface $deleteOldIndexException) { - throw new \RuntimeException( - sprintf( - 'Failed to delete old index %s with message: %s', - $oldIndexName, - $deleteOldIndexException->getMessage() - ) - ); - } - } - } - - /** - * Returns array of indexes which are mapped to given alias - * - * @param Index $esIndex ES Index - * @param string $aliasName Alias name - * - * @return array - */ - private function getAliasedIndexes(Index $esIndex, $aliasName) - { - $aliasesInfo = $esIndex->getClient()->request('_aliases', 'GET')->getData(); - $aliasedIndexes = array(); - - foreach ($aliasesInfo as $indexName => $indexInfo) { - $aliases = array_keys($indexInfo['aliases']); - if (in_array($aliasName, $aliases)) { - $aliasedIndexes[] = $indexName; - } - } - - return $aliasedIndexes; - } } diff --git a/Resources/config/config.xml b/Resources/config/config.xml index 4419b4a..06f0cda 100644 --- a/Resources/config/config.xml +++ b/Resources/config/config.xml @@ -1,94 +1,51 @@ + 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\Client - FOS\ElasticaBundle\DynamicIndex - Elastica\Type + FOS\ElasticaBundle\Elastica\Client FOS\ElasticaBundle\Logger\ElasticaLogger FOS\ElasticaBundle\DataCollector\ElasticaDataCollector - FOS\ElasticaBundle\Manager\RepositoryManager - FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerCollection - FOS\ElasticaBundle\Provider\ProviderRegistry + FOS\ElasticaBundle\Index\MappingBuilder Symfony\Component\PropertyAccess\PropertyAccessor - FOS\ElasticaBundle\Persister\ObjectPersister - FOS\ElasticaBundle\Persister\ObjectSerializerPersister - FOS\ElasticaBundle\Transformer\ModelToElasticaAutoTransformer - FOS\ElasticaBundle\Transformer\ModelToElasticaIdentifierTransformer + + + + + + + + + + + + + + + + + + + + + + + + %kernel.debug% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - diff --git a/Resources/config/index.xml b/Resources/config/index.xml new file mode 100644 index 0000000..11586ff --- /dev/null +++ b/Resources/config/index.xml @@ -0,0 +1,52 @@ + + + + + + FOS\ElasticaBundle\Index\AliasProcessor + FOS\ElasticaBundle\Finder\TransformedFinder + FOS\ElasticaBundle\Elastica\Index + FOS\ElasticaBundle\Provider\Indexable + FOS\ElasticaBundle\Index\IndexManager + FOS\ElasticaBundle\Index\Resetter + Elastica\Type + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/config/mongodb.xml b/Resources/config/mongodb.xml index 0af7aa1..8e15533 100644 --- a/Resources/config/mongodb.xml +++ b/Resources/config/mongodb.xml @@ -4,23 +4,32 @@ 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\Provider + FOS\ElasticaBundle\Doctrine\Listener + FOS\ElasticaBundle\Doctrine\MongoDB\ElasticaToModelTransformer + FOS\ElasticaBundle\Doctrine\RepositoryManager + - + + + + - + - - + + + - + @@ -29,11 +38,9 @@ - + - - diff --git a/Resources/config/orm.xml b/Resources/config/orm.xml index 5bd16e5..94f21d4 100644 --- a/Resources/config/orm.xml +++ b/Resources/config/orm.xml @@ -1,27 +1,34 @@ + 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\Provider + FOS\ElasticaBundle\Doctrine\Listener + FOS\ElasticaBundle\Doctrine\ORM\ElasticaToModelTransformer + FOS\ElasticaBundle\Doctrine\RepositoryManager + + - - + + - + - - - + + + - + @@ -30,11 +37,9 @@ - - - + + + - - diff --git a/Resources/config/persister.xml b/Resources/config/persister.xml new file mode 100644 index 0000000..8bd4dca --- /dev/null +++ b/Resources/config/persister.xml @@ -0,0 +1,27 @@ + + + + + + FOS\ElasticaBundle\Persister\ObjectPersister + FOS\ElasticaBundle\Persister\ObjectSerializerPersister + + + + + + + + + + + + + + + + + + diff --git a/Resources/config/propel.xml b/Resources/config/propel.xml index 7a7d93e..297e735 100644 --- a/Resources/config/propel.xml +++ b/Resources/config/propel.xml @@ -4,9 +4,9 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - + @@ -19,10 +19,8 @@ - + - - diff --git a/Resources/config/provider.xml b/Resources/config/provider.xml new file mode 100644 index 0000000..0732d54 --- /dev/null +++ b/Resources/config/provider.xml @@ -0,0 +1,18 @@ + + + + + + FOS\ElasticaBundle\Provider\ProviderRegistry + + + + + + + + + + diff --git a/Resources/config/serializer.xml b/Resources/config/serializer.xml new file mode 100644 index 0000000..8ee9646 --- /dev/null +++ b/Resources/config/serializer.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/Resources/config/source.xml b/Resources/config/source.xml new file mode 100644 index 0000000..c0f085c --- /dev/null +++ b/Resources/config/source.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/Resources/config/transformer.xml b/Resources/config/transformer.xml new file mode 100644 index 0000000..4ce5062 --- /dev/null +++ b/Resources/config/transformer.xml @@ -0,0 +1,32 @@ + + + + + + FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerCollection + FOS\ElasticaBundle\Transformer\ModelToElasticaAutoTransformer + FOS\ElasticaBundle\Transformer\ModelToElasticaIdentifierTransformer + + + + + + + + + + + + + + + + + + + + + + diff --git a/Resources/doc/cookbook/aliased-indexes.md b/Resources/doc/cookbook/aliased-indexes.md new file mode 100644 index 0000000..b9049c5 --- /dev/null +++ b/Resources/doc/cookbook/aliased-indexes.md @@ -0,0 +1,45 @@ +Aliased Indexes +=============== + +You can set up FOSElasticaBundle to use aliases for indexes which allows you to run an +index population without resetting the index currently being used by the application. + +> *Note*: When you're using an alias, resetting an individual type will still cause a +> reset for that type. + +To configure FOSElasticaBundle to use aliases for an index, set the use_alias option to +true. + +```yaml +fos_elastica: + indexes: + website: + use_alias: true +``` + +The process for setting up aliases on an existing application is slightly more complicated +because the bundle is not able to set an alias as the same name as an index. You have some +options on how to handle this: + +1) Delete the index from Elasticsearch. This option will make searching unavailable in your + application until a population has completed itself, and an alias is created. + +2) Change the index_name parameter for your index to something new, and manually alias the + current index to the new index_name, which will then be replaced when you run a repopulate. + +```yaml +fos_elastica: + indexes: + website: + use_alias: true + index_name: website_prod +``` + +```bash +$ curl -XPOST 'http://localhost:9200/_aliases' -d ' +{ + "actions" : [ + { "add" : { "index" : "website", "alias" : "website_prod" } } + ] +}' +``` diff --git a/Resources/doc/cookbook/custom-repositories.md b/Resources/doc/cookbook/custom-repositories.md new file mode 100644 index 0000000..9eff5f7 --- /dev/null +++ b/Resources/doc/cookbook/custom-repositories.md @@ -0,0 +1,72 @@ +##### Custom Repositories + +As well as the default repository you can create a custom repository for an entity and add +methods for particular searches. These need to extend `FOS\ElasticaBundle\Repository` to have +access to the finder: + +``` +find($query); + } +} +``` + +To use the custom repository specify it in the mapping for the entity: + + fos_elastica: + clients: + default: { host: localhost, port: 9200 } + indexes: + website: + client: default + types: + user: + mappings: + # your mappings + persistence: + driver: orm + model: Application\UserBundle\Entity\User + provider: ~ + finder: ~ + repository: Acme\ElasticaBundle\SearchRepository\UserRepository + +Then the custom queries will be available when using the repository returned from the manager: + + /** var FOS\ElasticaBundle\Manager\RepositoryManager */ + $repositoryManager = $container->get('fos_elastica.manager'); + + /** var FOS\ElasticaBundle\Repository */ + $repository = $repositoryManager->getRepository('UserBundle:User'); + + /** var array of Acme\UserBundle\Entity\User */ + $users = $repository->findWithCustomQuery('bob'); + +Alternatively you can specify the custom repository using an annotation in the entity: + +``` +userType = $userType; + } + + /** + * Insert the repository objects in the type index + * + * @param \Closure $loggerClosure + * @param array $options + */ + public function populate(\Closure $loggerClosure = null, array $options = array()) + { + if ($loggerClosure) { + $loggerClosure('Indexing users'); + } + + $document = new Document(); + $document->setData(array('username' => 'Bob')); + $this->userType->addDocuments(array($document)); + } +} +``` + +You will find a more complete implementation example in `src/FOS/ElasticaBundle/Doctrine/AbstractProvider.php`. diff --git a/Resources/doc/cookbook/multiple-connections.md b/Resources/doc/cookbook/multiple-connections.md new file mode 100644 index 0000000..7b5226c --- /dev/null +++ b/Resources/doc/cookbook/multiple-connections.md @@ -0,0 +1,16 @@ +Multiple Connections +==================== + +You can define multiple endpoints for an Elastica client by specifying them as +multiple connections in the client configuration: + +```yaml +fos_elastica: + clients: + default: + connections: + - url: http://es1.example.net:9200 + - url: http://es2.example.net:9200 +``` + +For more information on Elastica clustering see http://elastica.io/getting-started/installation.html#section-connect-cluster diff --git a/Resources/doc/cookbook/suppress-server-errors.md b/Resources/doc/cookbook/suppress-server-errors.md new file mode 100644 index 0000000..e4e371e --- /dev/null +++ b/Resources/doc/cookbook/suppress-server-errors.md @@ -0,0 +1,59 @@ +Suppressing Server Errors +========================= + +By default, exceptions from the Elastica client library will propagate through +the bundle's Client class. For instance, if the Elasticsearch server is offline, +issuing a request will result in an `Elastica\Exception\Connection` being thrown. +Depending on your needs, it may be desirable to suppress these exceptions and +allow searches to fail silently. + +One way to achieve this is to override the `fos_elastica.client.class` service +container parameter with a custom class. In the following example, we override +the `Client::request()` method and return the equivalent of an empty search +response if an exception occurred. + +Sample client code: +------------------- + +```php +_logger) { + $this->_logger->warning('Failed to send a request to ElasticSearch', array( + 'exception' => $e->getMessage(), + 'path' => $path, + 'method' => $method, + 'data' => $data, + 'query' => $query + )); + } + + return new Response('{"took":0,"timed_out":false,"hits":{"total":0,"max_score":0,"hits":[]}}'); + } + } +} +``` + +Configuration change: +--------------------- + +You must update a parameter in your `app/config/config.yml` file to point to your overridden client: + +```yaml +parameters: + fos_elastica.client.class: Acme\ElasticaBundle\Client +``` diff --git a/Resources/doc/index.md b/Resources/doc/index.md new file mode 100644 index 0000000..349723b --- /dev/null +++ b/Resources/doc/index.md @@ -0,0 +1,21 @@ +FOSElasticaBundle Documentation +=============================== + +Available documentation for FOSElasticaBundle +--------------------------------------------- + +* [Setup](setup.md) +* [Usage](usage.md) +* [Using a Serializer](serializer.md) +* [Types](types.md) + +Cookbook Entries +---------------- + +* [Aliased Indexes](cookbook/aliased-indexes.md) +* [Custom Repositories](cookbook/custom-repositories.md) +* [HTTP Headers for Elastica](cookbook/elastica-client-http-headers.md) +* Performance - [Logging](cookbook/logging.md) +* [Manual Providers](cookbook/manual-provider.md) +* [Clustering - Multiple Connections](cookbook/multiple-connections.md) +* [Suppressing server errors](cookbook/suppress-server-errors.md) diff --git a/Resources/doc/serializer.md b/Resources/doc/serializer.md new file mode 100644 index 0000000..05d958c --- /dev/null +++ b/Resources/doc/serializer.md @@ -0,0 +1,39 @@ +Using a Serializer in FOSElasticaBundle +======================================= + +FOSElasticaBundle supports using a Serializer component to serialize your objects to JSON +which will be sent directly to the Elasticsearch server. Combined with automatic mapping +it means types do not have to be mapped. + +A) Install and declare the serializer +------------------------- + +Follow the installation instructions for [JMSSerializerBundle](http://jmsyst.com/bundles/JMSSerializerBundle). + +Enable the serializer configuration for the bundle: + +```yaml +#app/config/config.yml +fos_elastica: + serializer: ~ +``` + +The default configuration that comes with FOSElasticaBundle supports both the JMS Serializer +and the Symfony Serializer. If JMSSerializerBundle is installed, additional support for +serialization groups and versions are added to the bundle. + +B) Set up each defined type to support serialization +---------------------------------------------------- + +A type does not need to have mappings defined when using a serializer. An example configuration +for a type in this case: + +```yaml +fos_elastica: + indexes: + search: + types: + user: + serializer: + groups: [elastica, Default] +``` diff --git a/Resources/doc/setup.md b/Resources/doc/setup.md new file mode 100644 index 0000000..6a1c2ae --- /dev/null +++ b/Resources/doc/setup.md @@ -0,0 +1,143 @@ +Step 1: Setting up the bundle +============================= + +A) Install FOSElasticaBundle +---------------------------- + +FOSElasticaBundle is installed using [Composer](https://getcomposer.org). + +```bash +$ php composer.phar require friendsofsymfony/elastica-bundle "~3.0" +``` + +### Elasticsearch + +Instructions for installing and deploying Elasticsearch may be found +[here](http://www.elasticsearch.org/guide/reference/setup/installation/). + + +B) Enable FOSElasticaBundle +--------------------------- + +Enable FOSElasticaBundle in your AppKernel: + +```php +enabled()` +* 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) +* 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 + information on the ExpressionLanguage component and its capabilities see its + [documentation](http://symfony.com/doc/current/components/expression_language/index.html) + +In all cases, the callback should return a true or false, with true indicating it will be +indexed, and a false indicating the object should not be indexed, or should be removed +from the index if we are running an update. + +Provider Configuration +---------------------- + +### Specifying a custom query builder for populating indexes + +When populating an index, it may be required to use a different query builder method +to define which entities should be queried. + +```yaml + user: + persistence: + provider: + query_builder_method: createIsActiveQueryBuilder +``` + +### Populating batch size + +By default, ElasticaBundle will index documents by packets of 100. +You can change this value in the provider configuration. + +```yaml + user: + persistence: + provider: + batch_size: 10 +``` + +### Changing the document identifier + +By default, ElasticaBundle will use the `id` field of your entities as +the Elasticsearch document identifier. You can change this value in the +persistence configuration. + +```yaml + user: + persistence: + identifier: searchId +``` + +### Turning on the persistence backend logger in production + +FOSElasticaBundle will turn of your persistence backend's logging configuration by default +when Symfony2 is not in debug mode. You can force FOSElasticaBundle to always disable +logging by setting debug_logging to false, to leave logging alone by setting it to true, +or leave it set to its default value which will mirror %kernel.debug%. + +```yaml + user: + persistence: + provider: + debug_logging: false +``` + +Listener Configuration +---------------------- + +### Realtime, selective index update + +If you use the Doctrine integration, you can let ElasticaBundle update the indexes automatically +when an object is added, updated or removed. It uses Doctrine lifecycle events. +Declare that you want to update the index in real time: + +```yaml + user: + persistence: + driver: orm + model: Application\UserBundle\Entity\User + listener: ~ # by default, listens to "insert", "update" and "delete" +``` + +Now the index is automatically updated each time the state of the bound Doctrine repository changes. +No need to repopulate the whole "user" index when a new `User` is created. + +You can also choose to only listen for some of the events: + +```yaml + persistence: + listener: + insert: true + update: false + delete: true +``` + +> **Propel** doesn't support this feature yet. + +Flushing Method +--------------- + +FOSElasticaBundle, since 3.0.0 performs its indexing in the postFlush Doctrine event +instead of prePersist and preUpdate which means that indexing will only occur when there +has been a successful flush. This new default makes more sense but in the instance where +you want to perform indexing before the flush is confirmed you may set the `immediate` +option on a type persistence configuration to `true`. + +```yaml + persistence: + listener: + immediate: true +``` + +Logging Errors +-------------- + +By default FOSElasticaBundle will not catch errors thrown by Elastica/ElasticSearch. +Configure a logger per listener if you would rather catch and log these. + +```yaml + persistence: + listener: + logger: true +``` + +Specifying `true` will use the default Elastica logger. Alternatively define your own +logger service id. diff --git a/Resources/doc/usage.md b/Resources/doc/usage.md new file mode 100644 index 0000000..37514b3 --- /dev/null +++ b/Resources/doc/usage.md @@ -0,0 +1,201 @@ +FOSElasticaBundle Usage +======================= + +Basic Searching with a Finder +----------------------------- + +The most useful searching method is to use a finder defined by the type configuration. +A finder will return results that have been hydrated by the configured persistence backend, +allowing you to use relationships of returned entities. For more information about +configuration options for this kind of searching, please see the [types](types.md) +documentation. + +```php +$finder = $this->container->get('fos_elastica.finder.search.user'); + +// Option 1. Returns all users who have example.net in any of their mapped fields +$results = $finder->find('example.net'); + +// Option 2. Returns a set of hybrid results that contain all Elasticsearch results +// and their transformed counterparts. Each result is an instance of a HybridResult +$results = $finder->findHybrid('example.net'); + +// Option 3a. Pagerfanta'd resultset +/** var Pagerfanta\Pagerfanta */ +$userPaginator = $finder->findPaginated('bob'); +$countOfResults = $userPaginator->getNbResults(); + +// Option 3b. KnpPaginator resultset + +``` + +Faceted Searching +----------------- + +When searching with facets, the facets can be retrieved when using the paginated +methods on the finder. + +```php +$query = new \Elastica\Query(); +$facet = new \Elastica\Facet\Terms('tags'); +$facet->setField('companyGroup'); +$query->addFacet($facet); + +$companies = $finder->findPaginated($query); +$companies->setMaxPerPage($params['limit']); +$companies->setCurrentPage($params['page']); + +$facets = $companies->getAdapter()->getFacets(); +``` + +Searching the entire index +-------------------------- + +You can also define a finder that will work on the entire index. Adjust your index +configuration as per below: + +```yaml +fos_elastica: + indexes: + website: + finder: ~ +``` + +You can now use the index wide finder service `fos_elastica.finder.website`: + +```php +/** var FOS\ElasticaBundle\Finder\MappedFinder */ +$finder = $this->container->get('fos_elastica.finder.website'); + +// Returns a mixed array of any objects mapped +$results = $finder->find('bob'); +``` + +Type Repositories +----------------- + +In the case where you need many different methods for different searching terms, it +may be better to separate methods for each type into their own dedicated repository +classes, just like Doctrine ORM's EntityRepository classes. + +The manager class that handles repositories has a service key of `fos_elastica.manager`. +The manager will default to handling ORM entities, and the configuration must be changed +for MongoDB users. + +```yaml +fos_elastica: + default_manager: mongodb +``` + +An example for using a repository: + +```php +/** var FOS\ElasticaBundle\Manager\RepositoryManager */ +$repositoryManager = $this->container->get('fos_elastica.manager'); + +/** var FOS\ElasticaBundle\Repository */ +$repository = $repositoryManager->getRepository('UserBundle:User'); + +/** var array of Acme\UserBundle\Entity\User */ +$users = $repository->find('bob'); +``` + +For more information about customising repositories, see the cookbook entry +[Custom Repositories](cookbook/custom-repositories.md). + +Using a custom query builder method for transforming results +------------------------------------------------------------ + +When returning results from Elasticsearch to be transformed by the bundle, the default +`createQueryBuilder` method on each objects Repository class will be called. In many +circumstances this is not ideal and you'd prefer to use a different method to join in +any entity relations that are required on the page that will be displaying the results. + +```yaml + user: + persistence: + elastica_to_model_transformer: + query_builder_method: createSearchQueryBuilder +``` + +An example for using a custom query builder method: + +```php +class UserRepository extends EntityRepository +{ + /** + * Used by Elastica to transform results to model + * + * @param string $entityAlias + * @return Doctrine\ORM\QueryBuilder + */ + public function createSearchQueryBuilder($entityAlias) + { + $qb = $this->createQueryBuilder($entityAlias); + + $qb->select($entityAlias, 'g') + ->innerJoin($entityAlias.'.groups', 'g'); + + return $qb; + } +} +``` + +Advanced Searching Example +-------------------------- + +If you would like to perform more advanced queries, here is one example using +the snowball stemming algorithm. + +It searches for Article entities using `title`, `tags`, and `categoryIds`. +Results must match at least one specified `categoryIds`, and should match the +`title` or `tags` criteria. Additionally, we define a snowball analyzer to +apply to queries against the `title` field. + +Assuming a type is configured as follows: + +```yaml +fos_elastica: + indexes: + site: + settings: + index: + analysis: + analyzer: + my_analyzer: + type: snowball + language: English + types: + article: + mappings: + title: { boost: 10, analyzer: my_analyzer } + tags: + categoryIds: + persistence: + driver: orm + model: Acme\DemoBundle\Entity\Article + provider: ~ + finder: ~ +``` + +The following code will execute a search against the Elasticsearch server: + +```php +$finder = $this->container->get('fos_elastica.finder.site.article'); +$boolQuery = new \Elastica\Query\Bool(); + +$fieldQuery = new \Elastica\Query\Match(); +$fieldQuery->setFieldQuery('title', 'I am a title string'); +$fieldQuery->setFieldParam('title', 'analyzer', 'my_analyzer'); +$boolQuery->addShould($fieldQuery); + +$tagsQuery = new \Elastica\Query\Terms(); +$tagsQuery->setTerms('tags', array('tag1', 'tag2')); +$boolQuery->addShould($tagsQuery); + +$categoryQuery = new \Elastica\Query\Terms(); +$categoryQuery->setTerms('categoryIds', array('1', '2', '3')); +$boolQuery->addMust($categoryQuery); + +$data = $finder->find($boolQuery); +``` diff --git a/Resources/views/Collector/elastica.html.twig b/Resources/views/Collector/elastica.html.twig index 637dae7..e6d7072 100644 --- a/Resources/views/Collector/elastica.html.twig +++ b/Resources/views/Collector/elastica.html.twig @@ -4,6 +4,9 @@ {% set icon %} elastica {{ collector.querycount }} + {% if collector.querycount > 0 %} + in {{ '%0.2f'|format(collector.time * 1000) }} ms + {% endif %} {% endset %} {% set text %}
@@ -24,6 +27,7 @@ Elastica {{ collector.querycount }} + {{ '%0.0f'|format(collector.time * 1000) }} ms {% endblock %} diff --git a/Serializer/Callback.php b/Serializer/Callback.php index 9fe7064..38d93dc 100644 --- a/Serializer/Callback.php +++ b/Serializer/Callback.php @@ -43,7 +43,7 @@ class Callback public function serialize($object) { - $context = $this->serializer instanceof SerializerInterface ? new SerializationContext() : array(); + $context = $this->serializer instanceof SerializerInterface ? SerializationContext::create()->enableMaxDepthChecks() : array(); if ($this->groups) { $context->setGroups($this->groups); diff --git a/Subscriber/PaginateElasticaQuerySubscriber.php b/Subscriber/PaginateElasticaQuerySubscriber.php index 82d30ea..e509cfa 100644 --- a/Subscriber/PaginateElasticaQuerySubscriber.php +++ b/Subscriber/PaginateElasticaQuerySubscriber.php @@ -2,6 +2,7 @@ namespace FOS\ElasticaBundle\Subscriber; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Knp\Component\Pager\Event\ItemsEvent; use FOS\ElasticaBundle\Paginator\PaginatorAdapterInterface; @@ -9,9 +10,19 @@ use FOS\ElasticaBundle\Paginator\PartialResultsInterface; class PaginateElasticaQuerySubscriber implements EventSubscriberInterface { + private $request; + + public function setRequest(Request $request = null) + { + $this->request = $request; + } + public function items(ItemsEvent $event) { if ($event->target instanceof PaginatorAdapterInterface) { + // Add sort to query + $this->setSorting($event); + /** @var $results PartialResultsInterface */ $results = $event->target->getResults($event->getOffset(), $event->getLimit()); @@ -30,6 +41,36 @@ class PaginateElasticaQuerySubscriber implements EventSubscriberInterface } } + /** + * Adds knp paging sort to query + * + * @param ItemsEvent $event + */ + protected function setSorting(ItemsEvent $event) + { + $options = $event->options; + $sortField = $this->request->get($options['sortFieldParameterName']); + + if (!empty($sortField)) { + // determine sort direction + $dir = 'asc'; + $sortDirection = $this->request->get($options['sortDirectionParameterName']); + if ('desc' === strtolower($sortDirection)) { + $dir = 'desc'; + } + + // check if the requested sort field is in the sort whitelist + if (isset($options['sortFieldWhitelist']) && !in_array($sortField, $options['sortFieldWhitelist'])) { + throw new \UnexpectedValueException(sprintf('Cannot sort by: [%s] this field is not in whitelist', $sortField)); + } + + // set sort on active query + $event->target->getQuery()->setSort(array( + $sortField => array('order' => $dir), + )); + } + } + public static function getSubscribedEvents() { return array( diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index 93143ec..7165052 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -11,126 +11,237 @@ use Symfony\Component\Config\Definition\Processor; class ConfigurationTest extends \PHPUnit_Framework_TestCase { /** - * @var Configuration + * @var Processor */ - private $configuration; + private $processor; public function setUp() { - $this->configuration = new Configuration(array()); + $this->processor = new Processor(); } - public function testEmptyConfigContainsFormatMappingOptionNode() + private function getConfigs(array $configArray) { - $tree = $this->configuration->getConfigTree(); - $children = $tree->getChildren(); - $children = $children['indexes']->getPrototype()->getChildren(); - $typeNodes = $children['types']->getPrototype()->getChildren(); - $mappings = $typeNodes['mappings']->getPrototype()->getChildren(); + $configuration = new Configuration(true); - $this->assertArrayHasKey('format', $mappings); - $this->assertInstanceOf('Symfony\Component\Config\Definition\ScalarNode', $mappings['format']); - $this->assertNull($mappings['format']->getDefaultValue()); + return $this->processor->processConfiguration($configuration, array($configArray)); } - public function testDynamicTemplateNodes() + public function testUnconfiguredConfiguration() { - $tree = $this->configuration->getConfigTree(); - $children = $tree->getChildren(); - $children = $children['indexes']->getPrototype()->getChildren(); - $typeNodes = $children['types']->getPrototype()->getChildren(); - $dynamicTemplates = $typeNodes['dynamic_templates']->getPrototype()->getChildren(); + $configuration = $this->getConfigs(array()); - $this->assertArrayHasKey('match', $dynamicTemplates); - $this->assertInstanceOf('Symfony\Component\Config\Definition\ScalarNode', $dynamicTemplates['match']); - $this->assertNull($dynamicTemplates['match']->getDefaultValue()); - - $this->assertArrayHasKey('match_mapping_type', $dynamicTemplates); - $this->assertInstanceOf('Symfony\Component\Config\Definition\ScalarNode', $dynamicTemplates['match_mapping_type']); - $this->assertNull($dynamicTemplates['match_mapping_type']->getDefaultValue()); - - $this->assertArrayHasKey('unmatch', $dynamicTemplates); - $this->assertInstanceOf('Symfony\Component\Config\Definition\ScalarNode', $dynamicTemplates['unmatch']); - $this->assertNull($dynamicTemplates['unmatch']->getDefaultValue()); - - $this->assertArrayHasKey('path_match', $dynamicTemplates); - $this->assertInstanceOf('Symfony\Component\Config\Definition\ScalarNode', $dynamicTemplates['path_match']); - $this->assertNull($dynamicTemplates['path_match']->getDefaultValue()); - - $this->assertArrayHasKey('path_unmatch', $dynamicTemplates); - $this->assertInstanceOf('Symfony\Component\Config\Definition\ScalarNode', $dynamicTemplates['path_unmatch']); - $this->assertNull($dynamicTemplates['path_unmatch']->getDefaultValue()); - - $this->assertArrayHasKey('match_pattern', $dynamicTemplates); - $this->assertInstanceOf('Symfony\Component\Config\Definition\ScalarNode', $dynamicTemplates['match_pattern']); - $this->assertNull($dynamicTemplates['match_pattern']->getDefaultValue()); - - $this->assertArrayHasKey('mapping', $dynamicTemplates); - $this->assertInstanceOf('Symfony\Component\Config\Definition\ArrayNode', $dynamicTemplates['mapping']); + $this->assertSame(array( + 'clients' => array(), + 'indexes' => array(), + 'default_manager' => 'orm' + ), $configuration); } - public function testDynamicTemplateMappingNodes() + public function testClientConfiguration() { - $tree = $this->configuration->getConfigTree(); - $children = $tree->getChildren(); - $children = $children['indexes']->getPrototype()->getChildren(); - $typeNodes = $children['types']->getPrototype()->getChildren(); - $dynamicTemplates = $typeNodes['dynamic_templates']->getPrototype()->getChildren(); - $mapping = $dynamicTemplates['mapping']->getChildren(); + $configuration = $this->getConfigs(array( + 'clients' => array( + 'default' => array( + 'url' => 'http://localhost:9200', + ), + 'clustered' => array( + 'connections' => array( + array( + 'url' => 'http://es1:9200', + 'headers' => array( + 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' + ) + ), + array( + 'url' => 'http://es2:9200', + 'headers' => array( + 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' + ) + ), + ) + ) + ) + )); - $this->assertArrayHasKey('type', $mapping); - $this->assertInstanceOf('Symfony\Component\Config\Definition\ScalarNode', $mapping['type']); - $this->assertSame('string', $mapping['type']->getDefaultValue()); + $this->assertCount(2, $configuration['clients']); + $this->assertCount(1, $configuration['clients']['default']['connections']); + $this->assertCount(0, $configuration['clients']['default']['connections'][0]['headers']); - $this->assertArrayHasKey('index', $mapping); - $this->assertInstanceOf('Symfony\Component\Config\Definition\ScalarNode', $mapping['index']); - $this->assertNull($mapping['index']->getDefaultValue()); + $this->assertCount(2, $configuration['clients']['clustered']['connections']); + $this->assertEquals('http://es2:9200/', $configuration['clients']['clustered']['connections'][1]['url']); + $this->assertCount(1, $configuration['clients']['clustered']['connections'][1]['headers']); + $this->assertEquals('Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', $configuration['clients']['clustered']['connections'][0]['headers'][0]); + } + + public function testLogging() + { + $configuration = $this->getConfigs(array( + 'clients' => array( + 'logging_enabled' => array( + 'url' => 'http://localhost:9200', + 'logger' => true, + ), + 'logging_disabled' => array( + 'url' => 'http://localhost:9200', + 'logger' => false, + ), + 'logging_not_mentioned' => array( + 'url' => 'http://localhost:9200', + ), + 'logging_custom' => array( + 'url' => 'http://localhost:9200', + 'logger' => 'custom.service' + ), + ) + )); + + $this->assertCount(4, $configuration['clients']); + + $this->assertEquals('fos_elastica.logger', $configuration['clients']['logging_enabled']['connections'][0]['logger']); + $this->assertFalse($configuration['clients']['logging_disabled']['connections'][0]['logger']); + $this->assertEquals('fos_elastica.logger', $configuration['clients']['logging_not_mentioned']['connections'][0]['logger']); + $this->assertEquals('custom.service', $configuration['clients']['logging_custom']['connections'][0]['logger']); } public function testSlashIsAddedAtTheEndOfServerUrl() { $config = array( 'clients' => array( - 'default' => array( - 'url' => 'http://www.github.com', - ), + 'default' => array('url' => 'http://www.github.com'), ), - ); - - $processor = new Processor(); + ); + $configuration = $this->getConfigs($config); - $configuration = $processor->processConfiguration($this->configuration, array($config)); - - $this->assertEquals('http://www.github.com/', $configuration['clients']['default']['servers'][0]['url']); + $this->assertEquals('http://www.github.com/', $configuration['clients']['default']['connections'][0]['url']); } - public function testEmptyFieldsIndexIsUnset() + public function testTypeConfig() { - $config = array( + $this->getConfigs(array( + 'clients' => array( + 'default' => array('url' => 'http://localhost:9200'), + ), 'indexes' => array( 'test' => array( + 'type_prototype' => array( + 'index_analyzer' => 'custom_analyzer', + 'persistence' => array( + 'identifier' => 'ID', + ), + 'serializer' => array( + 'groups' => array('Search'), + 'version' => 1 + ) + ), 'types' => array( 'test' => array( 'mappings' => array( - 'title' => array( - 'type' => 'string', - 'fields' => array( - 'autocomplete' => null - ) - ), - 'content' => null + 'title' => array(), + 'published' => array('type' => 'datetime'), + 'body' => null, + ), + 'persistence' => array( + 'listener' => array( + 'logger' => true, + ) + ) + ), + 'test2' => array( + 'mappings' => array( + 'title' => null, + 'children' => array( + 'type' => 'nested', + ) ) ) ) ) ) - ); + )); + } - $processor = new Processor(); + public function testClientConfigurationNoUrl() + { + $configuration = $this->getConfigs(array( + 'clients' => array( + 'default' => array( + 'host' => 'localhost', + 'port' => 9200, + ), + ) + )); - $configuration = $processor->processConfiguration(new Configuration(array($config)), array($config)); + $this->assertTrue(empty($configuration['clients']['default']['connections'][0]['url'])); + } - $this->assertArrayNotHasKey('fields', $configuration['indexes']['test']['types']['test']['mappings']['content']); - $this->assertArrayHasKey('fields', $configuration['indexes']['test']['types']['test']['mappings']['title']); + public function testMappingsRenamedToProperties() + { + $configuration = $this->getConfigs(array( + 'clients' => array( + 'default' => array('url' => 'http://localhost:9200'), + ), + 'indexes' => array( + 'test' => array( + 'types' => array( + 'test' => array( + 'mappings' => array( + 'title' => array(), + 'published' => array('type' => 'datetime'), + 'body' => null, + ) + ) + ) + ) + ) + )); + + $this->assertCount(3, $configuration['indexes']['test']['types']['test']['properties']); + } + + public function testNestedProperties() + { + $this->getConfigs(array( + 'clients' => array( + 'default' => array('url' => 'http://localhost:9200'), + ), + 'indexes' => array( + 'test' => array( + 'types' => array( + 'user' => array( + 'properties' => array( + 'field1' => array(), + ), + 'persistence' => array(), + ), + 'user_profile' => array( + '_parent' => array( + 'type' => 'user', + 'property' => 'owner', + ), + 'properties' => array( + 'field1' => array(), + 'field2' => array( + 'type' => 'nested', + 'properties' => array( + 'nested_field1' => array( + 'type' => 'integer' + ), + 'nested_field2' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer' + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + )); } } diff --git a/Tests/Doctrine/AbstractElasticaToModelTransformerTest.php b/Tests/Doctrine/AbstractElasticaToModelTransformerTest.php new file mode 100644 index 0000000..325171b --- /dev/null +++ b/Tests/Doctrine/AbstractElasticaToModelTransformerTest.php @@ -0,0 +1,66 @@ +getMock( + 'FOS\ElasticaBundle\Doctrine\ORM\ElasticaToModelTransformer', + array('findByIdentifiers'), + array($this->registry, $this->objectClass, array('ignore_missing' => true)) + ); + + $transformer->setPropertyAccessor(PropertyAccess::createPropertyAccessor()); + + $firstOrmResult = new \stdClass(); + $firstOrmResult->id = 1; + $secondOrmResult = new \stdClass(); + $secondOrmResult->id = 3; + $transformer->expects($this->once()) + ->method('findByIdentifiers') + ->with(array(1, 2, 3)) + ->willReturn(array($firstOrmResult, $secondOrmResult)); + + $firstElasticaResult = new Result(array('_id' => 1)); + $secondElasticaResult = new Result(array('_id' => 2)); + $thirdElasticaResult = new Result(array('_id' => 3)); + + $hybridResults = $transformer->hybridTransform(array($firstElasticaResult, $secondElasticaResult, $thirdElasticaResult)); + + $this->assertCount(2, $hybridResults); + $this->assertEquals($firstOrmResult, $hybridResults[0]->getTransformed()); + $this->assertEquals($firstElasticaResult, $hybridResults[0]->getResult()); + $this->assertEquals($secondOrmResult, $hybridResults[1]->getTransformed()); + $this->assertEquals($thirdElasticaResult, $hybridResults[1]->getResult()); + } + + 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 a9eff66..7242255 100644 --- a/Tests/Doctrine/AbstractListenerTest.php +++ b/Tests/Doctrine/AbstractListenerTest.php @@ -11,12 +11,12 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase { public function testObjectInsertedOnPersist() { - $persister = $this->getMockPersister(); - $entity = new Listener\Entity(1); + $persister = $this->getMockPersister($entity, 'index', 'type'); $eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager()); + $indexable = $this->getMockIndexable('index', 'type', $entity, true); - $listener = $this->createListener($persister, get_class($entity), array()); + $listener = $this->createListener($persister, array(), $indexable, array('indexName' => 'index', 'typeName' => 'type')); $listener->postPersist($eventArgs); $this->assertEquals($entity, current($listener->scheduledForInsertion)); @@ -28,18 +28,14 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase $listener->postFlush($eventArgs); } - /** - * @dataProvider provideIsIndexableCallbacks - */ - public function testNonIndexableObjectNotInsertedOnPersist($isIndexableCallback) + public function testNonIndexableObjectNotInsertedOnPersist() { - $persister = $this->getMockPersister(); - - $entity = new Listener\Entity(1, false); + $entity = new Listener\Entity(1); + $persister = $this->getMockPersister($entity, 'index', 'type'); $eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager()); + $indexable = $this->getMockIndexable('index', 'type', $entity, false); - $listener = $this->createListener($persister, get_class($entity), array()); - $listener->setIsIndexableCallback($isIndexableCallback); + $listener = $this->createListener($persister, array(), $indexable, array('indexName' => 'index', 'typeName' => 'type')); $listener->postPersist($eventArgs); $this->assertEmpty($listener->scheduledForInsertion); @@ -54,12 +50,12 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase public function testObjectReplacedOnUpdate() { - $persister = $this->getMockPersister(); - $entity = new Listener\Entity(1); + $persister = $this->getMockPersister($entity, 'index', 'type'); $eventArgs = $this->createLifecycleEventArgs($entity, $this->getMockObjectManager()); + $indexable = $this->getMockIndexable('index', 'type', $entity, true); - $listener = $this->createListener($persister, get_class($entity), array()); + $listener = $this->createListener($persister, array(), $indexable, array('indexName' => 'index', 'typeName' => 'type')); $listener->postUpdate($eventArgs); $this->assertEquals($entity, current($listener->scheduledForUpdate)); @@ -73,17 +69,15 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase $listener->postFlush($eventArgs); } - /** - * @dataProvider provideIsIndexableCallbacks - */ - public function testNonIndexableObjectRemovedOnUpdate($isIndexableCallback) + public function testNonIndexableObjectRemovedOnUpdate() { $classMetadata = $this->getMockClassMetadata(); $objectManager = $this->getMockObjectManager(); - $persister = $this->getMockPersister(); - $entity = new Listener\Entity(1, false); + $entity = new Listener\Entity(1); + $persister = $this->getMockPersister($entity, 'index', 'type'); $eventArgs = $this->createLifecycleEventArgs($entity, $objectManager); + $indexable = $this->getMockIndexable('index', 'type', $entity, false); $objectManager->expects($this->any()) ->method('getClassMetadata') @@ -95,18 +89,17 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase ->with($entity, 'id') ->will($this->returnValue($entity->getId())); - $listener = $this->createListener($persister, get_class($entity), array()); - $listener->setIsIndexableCallback($isIndexableCallback); + $listener = $this->createListener($persister, array(), $indexable, array('indexName' => 'index', 'typeName' => 'type')); $listener->postUpdate($eventArgs); $this->assertEmpty($listener->scheduledForUpdate); - $this->assertEquals($entity, current($listener->scheduledForDeletion)); + $this->assertEquals($entity->getId(), current($listener->scheduledForDeletion)); $persister->expects($this->never()) ->method('replaceOne'); $persister->expects($this->once()) - ->method('deleteMany') - ->with(array($entity)); + ->method('deleteManyByIdentifiers') + ->with(array($entity->getId())); $listener->postFlush($eventArgs); } @@ -115,10 +108,11 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase { $classMetadata = $this->getMockClassMetadata(); $objectManager = $this->getMockObjectManager(); - $persister = $this->getMockPersister(); $entity = new Listener\Entity(1); + $persister = $this->getMockPersister($entity, 'index', 'type'); $eventArgs = $this->createLifecycleEventArgs($entity, $objectManager); + $indexable = $this->getMockIndexable('index', 'type', $entity); $objectManager->expects($this->any()) ->method('getClassMetadata') @@ -130,14 +124,14 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase ->with($entity, 'id') ->will($this->returnValue($entity->getId())); - $listener = $this->createListener($persister, get_class($entity), array()); + $listener = $this->createListener($persister, array(), $indexable, array('indexName' => 'index', 'typeName' => 'type')); $listener->preRemove($eventArgs); - $this->assertEquals($entity, current($listener->scheduledForDeletion)); + $this->assertEquals($entity->getId(), current($listener->scheduledForDeletion)); $persister->expects($this->once()) - ->method('deleteMany') - ->with(array($entity)); + ->method('deleteManyByIdentifiers') + ->with(array($entity->getId())); $listener->postFlush($eventArgs); } @@ -146,10 +140,12 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase { $classMetadata = $this->getMockClassMetadata(); $objectManager = $this->getMockObjectManager(); - $persister = $this->getMockPersister(); $entity = new Listener\Entity(1); + $entity->identifier = 'foo'; + $persister = $this->getMockPersister($entity, 'index', 'type'); $eventArgs = $this->createLifecycleEventArgs($entity, $objectManager); + $indexable = $this->getMockIndexable('index', 'type', $entity); $objectManager->expects($this->any()) ->method('getClassMetadata') @@ -161,54 +157,30 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase ->with($entity, 'identifier') ->will($this->returnValue($entity->getId())); - $listener = $this->createListener($persister, get_class($entity), array(), 'identifier'); + $listener = $this->createListener($persister, array(), $indexable, array('identifier' => 'identifier', 'indexName' => 'index', 'typeName' => 'type')); $listener->preRemove($eventArgs); - $this->assertEquals($entity, current($listener->scheduledForDeletion)); + $this->assertEquals($entity->identifier, current($listener->scheduledForDeletion)); $persister->expects($this->once()) - ->method('deleteMany') - ->with(array($entity)); + ->method('deleteManyByIdentifiers') + ->with(array($entity->identifier)); $listener->postFlush($eventArgs); } - /** - * @dataProvider provideInvalidIsIndexableCallbacks - * @expectedException \RuntimeException - */ - public function testInvalidIsIndexableCallbacks($isIndexableCallback) - { - $listener = $this->createListener($this->getMockPersister(), 'FOS\ElasticaBundle\Tests\Doctrine\Listener\Entity', array()); - $listener->setIsIndexableCallback($isIndexableCallback); - } - - public function provideInvalidIsIndexableCallbacks() - { - return array( - array('nonexistentEntityMethod'), - array(array(new Listener\IndexableDecider(), 'internalMethod')), - array(42), - array('entity.getIsIndexable() && nonexistentEntityFunction()'), - ); - } - - public function provideIsIndexableCallbacks() - { - return array( - array('getIsIndexable'), - array(array(new Listener\IndexableDecider(), 'isIndexable')), - array(function(Listener\Entity $entity) { return $entity->getIsIndexable(); }), - array('entity.getIsIndexable()') - ); - } - abstract protected function getLifecycleEventArgsClass(); abstract protected function getListenerClass(); + /** + * @return string + */ abstract protected function getObjectManagerClass(); + /** + * @return string + */ abstract protected function getClassMetadataClass(); private function createLifecycleEventArgs() @@ -239,9 +211,59 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase ->getMock(); } - private function getMockPersister() + /** + * @param Listener\Entity $object + * @param string $indexName + * @param string $typeName + */ + private function getMockPersister($object, $indexName, $typeName) { - return $this->getMock('FOS\ElasticaBundle\Persister\ObjectPersisterInterface'); + $mock = $this->getMockBuilder('FOS\ElasticaBundle\Persister\ObjectPersister') + ->disableOriginalConstructor() + ->getMock(); + + $mock->expects($this->any()) + ->method('handlesObject') + ->with($object) + ->will($this->returnValue(true)); + + $index = $this->getMockBuilder('Elastica\Index')->disableOriginalConstructor()->getMock(); + $index->expects($this->any()) + ->method('getName') + ->will($this->returnValue($indexName)); + $type = $this->getMockBuilder('Elastica\Type')->disableOriginalConstructor()->getMock(); + $type->expects($this->any()) + ->method('getName') + ->will($this->returnValue($typeName)); + $type->expects($this->any()) + ->method('getIndex') + ->will($this->returnValue($index)); + + $mock->expects($this->any()) + ->method('getType') + ->will($this->returnValue($type)); + + 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'); + + if (null !== $return) { + $mock->expects($this->once()) + ->method('isObjectIndexable') + ->with($indexName, $typeName, $object) + ->will($this->returnValue($return)); + } + + return $mock; } } @@ -250,33 +272,18 @@ namespace FOS\ElasticaBundle\Tests\Doctrine\Listener; class Entity { private $id; - private $isIndexable; - public function __construct($id, $isIndexable = true) + /** + * @param integer $id + */ + public function __construct($id) { $this->id = $id; - $this->isIndexable = $isIndexable; } public function getId() { return $this->id; } - - public function getIsIndexable() - { - return $this->isIndexable; - } } -class IndexableDecider -{ - public function isIndexable(Entity $entity) - { - return $entity->getIsIndexable(); - } - - protected function internalMethod() - { - } -} diff --git a/Tests/Doctrine/AbstractProviderTest.php b/Tests/Doctrine/AbstractProviderTest.php index 2492eed..99ed2de 100644 --- a/Tests/Doctrine/AbstractProviderTest.php +++ b/Tests/Doctrine/AbstractProviderTest.php @@ -2,6 +2,9 @@ namespace FOS\ElasticaBundle\Tests\Doctrine; +use Elastica\Bulk\ResponseSet; +use Elastica\Response; + class AbstractProviderTest extends \PHPUnit_Framework_TestCase { private $objectClass; @@ -9,24 +12,26 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase private $objectPersister; private $options; private $managerRegistry; + private $indexable; public function setUp() { - if (!interface_exists('Doctrine\Common\Persistence\ManagerRegistry')) { - $this->markTestSkipped('Doctrine Common is not available.'); - } + if (!interface_exists('Doctrine\Common\Persistence\ManagerRegistry')) { + $this->markTestSkipped('Doctrine Common is not available.'); + } - $this->objectClass = 'objectClass'; - $this->options = array(); + $this->objectClass = 'objectClass'; + $this->options = array('debug_logging' => true, 'indexName' => 'index', 'typeName' => 'type'); - $this->objectPersister = $this->getMockObjectPersister(); - $this->managerRegistry = $this->getMockManagerRegistry(); - $this->objectManager = $this->getMockObjectManager(); + $this->objectPersister = $this->getMockObjectPersister(); + $this->managerRegistry = $this->getMockManagerRegistry(); + $this->objectManager = $this->getMockObjectManager(); + $this->indexable = $this->getMockIndexable(); - $this->managerRegistry->expects($this->any()) - ->method('getManagerForClass') - ->with($this->objectClass) - ->will($this->returnValue($this->objectManager)); + $this->managerRegistry->expects($this->any()) + ->method('getManagerForClass') + ->with($this->objectClass) + ->will($this->returnValue($this->objectManager)); } /** @@ -49,6 +54,11 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->with($queryBuilder) ->will($this->returnValue($nbObjects)); + $this->indexable->expects($this->any()) + ->method('isObjectIndexable') + ->with('index', 'type', $this->anything()) + ->will($this->returnValue(true)); + $providerInvocationOffset = 2; foreach ($objectsByIteration as $i => $objects) { @@ -59,14 +69,13 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->with($queryBuilder, $batchSize, $offset) ->will($this->returnValue($objects)); - $this->objectPersister->expects($this->at($i)) - ->method('insertMany') - ->with($objects); - $this->objectManager->expects($this->at($i)) ->method('clear'); } + $this->objectPersister->expects($this->exactly(count($objectsByIteration))) + ->method('insertMany'); + $provider->populate(); } @@ -102,6 +111,11 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->method('fetchSlice') ->will($this->returnValue($objects)); + $this->indexable->expects($this->any()) + ->method('isObjectIndexable') + ->with('index', 'type', $this->anything()) + ->will($this->returnValue(true)); + $this->objectManager->expects($this->never()) ->method('clear'); @@ -123,6 +137,11 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->method('fetchSlice') ->will($this->returnValue($objects)); + $this->indexable->expects($this->any()) + ->method('isObjectIndexable') + ->with('index', 'type', $this->anything()) + ->will($this->returnValue(true)); + $loggerClosureInvoked = false; $loggerClosure = function () use (&$loggerClosureInvoked) { $loggerClosureInvoked = true; @@ -150,6 +169,11 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->method('fetchSlice') ->will($this->returnValue($objects)); + $this->indexable->expects($this->any()) + ->method('isObjectIndexable') + ->with('index', 'type', $this->anything()) + ->will($this->returnValue(true)); + $this->objectPersister->expects($this->any()) ->method('insertMany') ->will($this->throwException($this->getMockBulkResponseException())); @@ -159,6 +183,36 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase $provider->populate(null, array('ignore-errors' => false)); } + public function testPopulateRunsIndexCallable() + { + $nbObjects = 2; + $objects = array(1, 2); + + $provider = $this->getMockAbstractProvider(); + $provider->expects($this->any()) + ->method('countObjects') + ->will($this->returnValue($nbObjects)); + $provider->expects($this->any()) + ->method('fetchSlice') + ->will($this->returnValue($objects)); + + $this->indexable->expects($this->at(0)) + ->method('isObjectIndexable') + ->with('index', 'type', 1) + ->will($this->returnValue(false)); + $this->indexable->expects($this->at(1)) + ->method('isObjectIndexable') + ->with('index', 'type', 2) + ->will($this->returnValue(true)); + + + $this->objectPersister->expects($this->once()) + ->method('insertMany') + ->with(array(1 => 2)); + + $provider->populate(); + } + /** * @return \FOS\ElasticaBundle\Doctrine\AbstractProvider|\PHPUnit_Framework_MockObject_MockObject */ @@ -166,6 +220,7 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase { return $this->getMockForAbstractClass('FOS\ElasticaBundle\Doctrine\AbstractProvider', array( $this->objectPersister, + $this->indexable, $this->objectClass, $this->options, $this->managerRegistry, @@ -177,9 +232,9 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase */ private function getMockBulkResponseException() { - return $this->getMockBuilder('Elastica\Exception\Bulk\ResponseException') - ->disableOriginalConstructor() - ->getMock(); + return $this->getMock('Elastica\Exception\Bulk\ResponseException', null, array( + new ResponseSet(new Response(array()), array()) + )); } /** @@ -205,6 +260,14 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase { return $this->getMock('FOS\ElasticaBundle\Persister\ObjectPersisterInterface'); } + + /** + * @return \FOS\ElasticaBundle\Provider\IndexableInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private function getMockIndexable() + { + return $this->getMock('FOS\ElasticaBundle\Provider\IndexableInterface'); + } } /** diff --git a/Tests/ClientTest.php b/Tests/Elastica/ClientTest.php similarity index 86% rename from Tests/ClientTest.php rename to Tests/Elastica/ClientTest.php index 8a9d91a..43ac7a2 100644 --- a/Tests/ClientTest.php +++ b/Tests/Elastica/ClientTest.php @@ -1,11 +1,11 @@ isType('array') ); - $client = $this->getMockBuilder('FOS\ElasticaBundle\Client') + $client = $this->getMockBuilder('FOS\ElasticaBundle\Elastica\Client') ->setMethods(array('getConnection')) ->getMock(); diff --git a/Tests/FOSElasticaBundleTest.php b/Tests/FOSElasticaBundleTest.php index 2bfc7f9..4290e1d 100644 --- a/Tests/FOSElasticaBundleTest.php +++ b/Tests/FOSElasticaBundleTest.php @@ -3,37 +3,21 @@ namespace FOS\ElasticaBundle\Tests\Resetter; use FOS\ElasticaBundle\FOSElasticaBundle; -use Symfony\Component\DependencyInjection\Compiler\PassConfig; class FOSElasticaBundleTest extends \PHPUnit_Framework_TestCase { public function testCompilerPassesAreRegistered() { - $passes = array( - array ( - 'FOS\ElasticaBundle\DependencyInjection\Compiler\RegisterProvidersPass', - PassConfig::TYPE_BEFORE_REMOVING - ), - array ( - 'FOS\ElasticaBundle\DependencyInjection\Compiler\TransformerPass' - ), - ); - $container = $this ->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); $container - ->expects($this->at(0)) + ->expects($this->atLeastOnce()) ->method('addCompilerPass') - ->with($this->isInstanceOf($passes[0][0]), $passes[0][1]); + ->with($this->isInstanceOf('Symfony\\Component\\DependencyInjection\\Compiler\\CompilerPassInterface')); - $container - ->expects($this->at(1)) - ->method('addCompilerPass') - ->with($this->isInstanceOf($passes[1][0])); $bundle = new FOSElasticaBundle(); - $bundle->build($container); } } diff --git a/Tests/Functional/ConfigurationManagerTest.php b/Tests/Functional/ConfigurationManagerTest.php new file mode 100644 index 0000000..7ef02c5 --- /dev/null +++ b/Tests/Functional/ConfigurationManagerTest.php @@ -0,0 +1,58 @@ + + * + * 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 ConfigurationManagerTest extends WebTestCase +{ + public function testContainerSource() + { + $client = $this->createClient(array('test_case' => 'Basic')); + $manager = $this->getManager($client); + + $index = $manager->getIndexConfiguration('index'); + + $this->assertEquals('index', $index->getName()); + $this->assertGreaterThanOrEqual(2, count($index->getTypes())); + $this->assertInstanceOf('FOS\\ElasticaBundle\\Configuration\\TypeConfig', $index->getType('type')); + $this->assertInstanceOf('FOS\\ElasticaBundle\\Configuration\\TypeConfig', $index->getType('parent')); + } + + protected function setUp() + { + parent::setUp(); + + $this->deleteTmpDir('Basic'); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->deleteTmpDir('Basic'); + } + + /** + * @param Client $client + * @return \FOS\ElasticaBundle\Configuration\ConfigManager + */ + private function getManager(Client $client) + { + $manager = $client->getContainer()->get('fos_elastica.config_manager'); + + return $manager; + } +} diff --git a/Tests/Functional/IndexableCallbackTest.php b/Tests/Functional/IndexableCallbackTest.php new file mode 100644 index 0000000..41ed402 --- /dev/null +++ b/Tests/Functional/IndexableCallbackTest.php @@ -0,0 +1,52 @@ + + * + * 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; + +/** + * @group functional + */ +class IndexableCallbackTest extends WebTestCase +{ + /** + * 2 reasons for this test: + * + * 1) To test that the configuration rename from is_indexable_callback under the listener + * key is respected, and + * 2) To test the Extension's set up of the Indexable service. + */ + public function testIndexableCallback() + { + $client = $this->createClient(array('test_case' => 'ORM')); + + /** @var \FOS\ElasticaBundle\Provider\Indexable $in */ + $in = $client->getContainer()->get('fos_elastica.indexable'); + + $this->assertTrue($in->isObjectIndexable('index', 'type', new TypeObj())); + $this->assertTrue($in->isObjectIndexable('index', 'type2', new TypeObj())); + $this->assertFalse($in->isObjectIndexable('index', 'type3', new TypeObj())); + $this->assertFalse($in->isObjectIndexable('index', 'type4', new TypeObj())); + } + + protected function setUp() + { + parent::setUp(); + + $this->deleteTmpDir('ORM'); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->deleteTmpDir('ORM'); + } +} diff --git a/Tests/Functional/MappingToElasticaTest.php b/Tests/Functional/MappingToElasticaTest.php new file mode 100644 index 0000000..f42df61 --- /dev/null +++ b/Tests/Functional/MappingToElasticaTest.php @@ -0,0 +1,130 @@ + + * + * 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 MappingToElasticaTest extends WebTestCase +{ + public function testResetIndexAddsMappings() + { + $client = $this->createClient(array('test_case' => 'Basic')); + $resetter = $this->getResetter($client); + $resetter->resetIndex('index'); + + $type = $this->getType($client); + $mapping = $type->getMapping(); + + $this->assertNotEmpty($mapping, 'Mapping was populated'); + $this->assertArrayHasKey('store', $mapping['type']['properties']['field1']); + $this->assertTrue($mapping['type']['properties']['field1']['store']); + $this->assertArrayNotHasKey('store', $mapping['type']['properties']['field2']); + + $parent = $this->getType($client, 'parent'); + $mapping = $parent->getMapping(); + + $this->assertEquals('my_analyzer', $mapping['parent']['index_analyzer']); + $this->assertEquals('whitespace', $mapping['parent']['search_analyzer']); + } + + public function testResetType() + { + $client = $this->createClient(array('test_case' => 'Basic')); + $resetter = $this->getResetter($client); + $resetter->resetIndexType('index', 'type'); + + $type = $this->getType($client); + $mapping = $type->getMapping(); + + $this->assertNotEmpty($mapping, 'Mapping was populated'); + $this->assertArrayHasKey('store', $mapping['type']['properties']['field1']); + $this->assertTrue($mapping['type']['properties']['field1']['store']); + $this->assertArrayNotHasKey('store', $mapping['type']['properties']['field2']); + } + + public function testORMResetIndexAddsMappings() + { + $client = $this->createClient(array('test_case' => 'ORM')); + $resetter = $this->getResetter($client); + $resetter->resetIndex('index'); + + $type = $this->getType($client); + $mapping = $type->getMapping(); + + $this->assertNotEmpty($mapping, 'Mapping was populated'); + } + + public function testORMResetType() + { + $client = $this->createClient(array('test_case' => 'ORM')); + $resetter = $this->getResetter($client); + $resetter->resetIndexType('index', 'type'); + + $type = $this->getType($client); + $mapping = $type->getMapping(); + + $this->assertNotEmpty($mapping, 'Mapping was populated'); + } + + public function testMappingIteratorToArrayField() + { + $client = $this->createClient(array('test_case' => 'ORM')); + $persister = $client->getContainer()->get('fos_elastica.object_persister.index.type'); + + $object = new TypeObj(); + $object->id = 1; + $object->coll = new \ArrayIterator(array('foo', 'bar')); + $persister->insertOne($object); + + $object->coll = new \ArrayIterator(array('foo', 'bar', 'bazz')); + $object->coll->offsetUnset(1); + + $persister->replaceOne($object); + } + + /** + * @param Client $client + * @return \FOS\ElasticaBundle\Resetter $resetter + */ + private function getResetter(Client $client) + { + return $client->getContainer()->get('fos_elastica.resetter'); + } + + /** + * @param Client $client + * @return \Elastica\Type + */ + private function getType(Client $client, $type = 'type') + { + return $client->getContainer()->get('fos_elastica.index.index.' . $type); + } + + protected function setUp() + { + parent::setUp(); + + $this->deleteTmpDir('Basic'); + $this->deleteTmpDir('ORM'); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->deleteTmpDir('Basic'); + $this->deleteTmpDir('ORM'); + } +} diff --git a/Tests/Functional/SerializerTest.php b/Tests/Functional/SerializerTest.php new file mode 100644 index 0000000..81fbc8f --- /dev/null +++ b/Tests/Functional/SerializerTest.php @@ -0,0 +1,55 @@ + + * + * 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; + +/** + * @group functional + */ +class SerializerTest extends WebTestCase +{ + public function testMappingIteratorToArrayField() + { + $client = $this->createClient(array('test_case' => 'Serializer')); + $persister = $client->getContainer()->get('fos_elastica.object_persister.index.type'); + + $object = new TypeObj(); + $object->id = 1; + $object->coll = new \ArrayIterator(array('foo', 'bar')); + $persister->insertOne($object); + + $object->coll = new \ArrayIterator(array('foo', 'bar', 'bazz')); + $object->coll->offsetUnset(1); + + $persister->replaceOne($object); + } + + public function testUnmappedType() + { + $client = $this->createClient(array('test_case' => 'Serializer')); + $resetter = $client->getContainer()->get('fos_elastica.resetter'); + $resetter->resetIndex('index'); + } + + protected function setUp() + { + parent::setUp(); + + $this->deleteTmpDir('Serializer'); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->deleteTmpDir('Serializer'); + } +} diff --git a/Tests/Functional/TypeObj.php b/Tests/Functional/TypeObj.php new file mode 100644 index 0000000..46e5968 --- /dev/null +++ b/Tests/Functional/TypeObj.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Tests\Functional; + +class TypeObj +{ + public $coll; + public $field1; + + public function isIndexable() + { + return true; + } + + public function isntIndexable() + { + return false; + } + + public function getSerializableColl() + { + return iterator_to_array($this->coll, false); + } +} diff --git a/Tests/Functional/WebTestCase.php b/Tests/Functional/WebTestCase.php new file mode 100644 index 0000000..38f5489 --- /dev/null +++ b/Tests/Functional/WebTestCase.php @@ -0,0 +1,40 @@ + + * + * 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\Tests\Functional\WebTestCase as BaseWebTestCase; + +class WebTestCase extends BaseWebTestCase +{ + protected static function getKernelClass() + { + require_once __DIR__.'/app/AppKernel.php'; + + return 'FOS\ElasticaBundle\Tests\Functional\app\AppKernel'; + } + + protected static function createKernel(array $options = array()) + { + $class = self::getKernelClass(); + + if (!isset($options['test_case'])) { + throw new \InvalidArgumentException('The option "test_case" must be set.'); + } + + return new $class( + $options['test_case'], + isset($options['root_config']) ? $options['root_config'] : 'config.yml', + isset($options['environment']) ? $options['environment'] : 'foselasticabundle'.strtolower($options['test_case']), + isset($options['debug']) ? $options['debug'] : true + ); + } +} diff --git a/Tests/Functional/app/AppKernel.php b/Tests/Functional/app/AppKernel.php new file mode 100644 index 0000000..f47a5b3 --- /dev/null +++ b/Tests/Functional/app/AppKernel.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Tests\Functional\app; + +// get the autoload file +$dir = __DIR__; +$lastDir = null; +while ($dir !== $lastDir) { + $lastDir = $dir; + + if (file_exists($dir.'/autoload.php')) { + require_once $dir.'/autoload.php'; + break; + } + + if (file_exists($dir.'/autoload.php.dist')) { + require_once $dir.'/autoload.php.dist'; + break; + } + + if (file_exists($dir.'/vendor/autoload.php')) { + require_once $dir.'/vendor/autoload.php'; + break; + } + + $dir = dirname($dir); +} + +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpKernel\Kernel; + +/** + * App Test Kernel for functional tests. + * + * @author Johannes M. Schmitt + */ +class AppKernel extends Kernel +{ + private $testCase; + private $rootConfig; + + public function __construct($testCase, $rootConfig, $environment, $debug) + { + if (!is_dir(__DIR__.'/'.$testCase)) { + throw new \InvalidArgumentException(sprintf('The test case "%s" does not exist.', $testCase)); + } + $this->testCase = $testCase; + + $fs = new Filesystem(); + if (!$fs->isAbsolutePath($rootConfig) && !file_exists($rootConfig = __DIR__.'/'.$testCase.'/'.$rootConfig)) { + throw new \InvalidArgumentException(sprintf('The root config "%s" does not exist.', $rootConfig)); + } + $this->rootConfig = $rootConfig; + + parent::__construct($environment, $debug); + } + + public function registerBundles() + { + if (!file_exists($filename = $this->getRootDir().'/'.$this->testCase.'/bundles.php')) { + throw new \RuntimeException(sprintf('The bundles file "%s" does not exist.', $filename)); + } + + return include $filename; + } + + public function init() + { + } + + public function getRootDir() + { + return __DIR__; + } + + public function getCacheDir() + { + return sys_get_temp_dir().'/'.Kernel::VERSION.'/'.$this->testCase.'/cache/'.$this->environment; + } + + public function getLogDir() + { + return sys_get_temp_dir().'/'.Kernel::VERSION.'/'.$this->testCase.'/logs'; + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load($this->rootConfig); + } + + public function serialize() + { + return serialize(array($this->testCase, $this->rootConfig, $this->getEnvironment(), $this->isDebug())); + } + + public function unserialize($str) + { + call_user_func_array(array($this, '__construct'), unserialize($str)); + } + + protected function getKernelParameters() + { + $parameters = parent::getKernelParameters(); + $parameters['kernel.test_case'] = $this->testCase; + + return $parameters; + } +} \ No newline at end of file diff --git a/Tests/Functional/app/Basic/bundles.php b/Tests/Functional/app/Basic/bundles.php new file mode 100644 index 0000000..7bcaae8 --- /dev/null +++ b/Tests/Functional/app/Basic/bundles.php @@ -0,0 +1,13 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Tests\Functional\app\ORM; + +class IndexableService +{ + public function isIndexable($object) + { + return true; + } + + public static function isntIndexable($object) + { + return false; + } +} diff --git a/Tests/Functional/app/ORM/bundles.php b/Tests/Functional/app/ORM/bundles.php new file mode 100644 index 0000000..25db3fe --- /dev/null +++ b/Tests/Functional/app/ORM/bundles.php @@ -0,0 +1,13 @@ +getMockBuilder('FOS\\ElasticaBundle\\Elastica\\Index') + ->disableOriginalConstructor() + ->getMock(); + + $index->expects($this->any()) + ->method('getName') + ->will($this->returnValue($indexName)); + + $this->indexes[$indexName] = $index; + } + + $this->indexManager = new IndexManager($this->indexes, $this->indexes['index2']); + } + + public function testGetAllIndexes() + { + $this->assertEquals($this->indexes, $this->indexManager->getAllIndexes()); + } + + public function testGetIndex() + { + $this->assertEquals($this->indexes['index1'], $this->indexManager->getIndex('index1')); + $this->assertEquals($this->indexes['index2'], $this->indexManager->getIndex('index2')); + $this->assertEquals($this->indexes['index3'], $this->indexManager->getIndex('index3')); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testGetIndexShouldThrowExceptionForInvalidName() + { + $this->indexManager->getIndex('index4'); + } + + public function testGetDefaultIndex() + { + $this->assertEquals('index2', $this->indexManager->getIndex()->getName()); + $this->assertEquals('index2', $this->indexManager->getDefaultIndex()->getName()); + } +} diff --git a/Tests/ResetterTest.php b/Tests/Index/ResetterTest.php similarity index 71% rename from Tests/ResetterTest.php rename to Tests/Index/ResetterTest.php index b4e5649..28f0a68 100644 --- a/Tests/ResetterTest.php +++ b/Tests/Index/ResetterTest.php @@ -1,24 +1,49 @@ indexConfigsByName = array( + $this->markTestIncomplete('To be rewritten'); + $this->configManager = $this->getMockBuilder('FOS\\ElasticaBundle\\Configuration\\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + $this->indexManager = $this->getMockBuilder('FOS\\ElasticaBundle\\Index\\IndexManager') + ->disableOriginalConstructor() + ->getMock(); + $this->aliasProcessor = $this->getMockBuilder('FOS\\ElasticaBundle\\Index\\AliasProcessor') + ->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( - 'mappings' => array( + 'properties' => array( 'a' => array( 'dynamic_templates' => array(), 'properties' => array(), @@ -30,7 +55,7 @@ class ResetterTest extends \PHPUnit_Framework_TestCase 'bar' => array( 'index' => $this->getMockElasticaIndex(), 'config' => array( - 'mappings' => array( + 'properties' => array( 'a' => array('properties' => array()), 'b' => array('properties' => array()), ), @@ -39,7 +64,7 @@ class ResetterTest extends \PHPUnit_Framework_TestCase 'parent' => array( 'index' => $this->getMockElasticaIndex(), 'config' => array( - 'mappings' => array( + 'properties' => array( 'a' => array( 'properties' => array( 'field_2' => array() @@ -54,12 +79,26 @@ class ResetterTest extends \PHPUnit_Framework_TestCase ), ), ), - ); + );*/ } public function testResetAllIndexes() { - $this->indexConfigsByName['foo']['index']->expects($this->once()) + $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); @@ -67,8 +106,8 @@ class ResetterTest extends \PHPUnit_Framework_TestCase ->method('create') ->with($this->indexConfigsByName['bar']['config'], true); - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetAllIndexes(); + $resetter = new Resetter($this->indexConfigsByName);*/ + $this->resetter->resetAllIndexes(); } public function testResetIndex() @@ -105,8 +144,8 @@ class ResetterTest extends \PHPUnit_Framework_TestCase $type->expects($this->once()) ->method('delete'); - $mapping = Mapping::create($this->indexConfigsByName['foo']['config']['mappings']['a']['properties']); - $mapping->setParam('dynamic_templates', $this->indexConfigsByName['foo']['config']['mappings']['a']['dynamic_templates']); + $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); @@ -149,8 +188,8 @@ class ResetterTest extends \PHPUnit_Framework_TestCase new Response(array('error' => 'TypeMissingException[[de_20131022] type[bla] missing]', 'status' => 404))) )); - $mapping = Mapping::create($this->indexConfigsByName['foo']['config']['mappings']['a']['properties']); - $mapping->setParam('dynamic_templates', $this->indexConfigsByName['foo']['config']['mappings']['a']['dynamic_templates']); + $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); @@ -171,7 +210,7 @@ class ResetterTest extends \PHPUnit_Framework_TestCase $type->expects($this->once()) ->method('delete'); - $mapping = Mapping::create($this->indexConfigsByName['parent']['config']['mappings']['a']['properties']); + $mapping = Mapping::create($this->indexConfigsByName['parent']['config']['properties']['a']['properties']); $mapping->setParam('_parent', array('type' => 'b')); $type->expects($this->once()) ->method('setMapping') diff --git a/Tests/IndexManagerTest.php b/Tests/IndexManagerTest.php deleted file mode 100644 index 0a8ea37..0000000 --- a/Tests/IndexManagerTest.php +++ /dev/null @@ -1,58 +0,0 @@ -defaultIndexName = 'index2'; - $this->indexesByName = array( - 'index1' => 'test1', - 'index2' => 'test2', - ); - - /** @var $defaultIndex \PHPUnit_Framework_MockObject_MockObject|\Elastica\Index */ - $defaultIndex = $this->getMockBuilder('Elastica\Index') - ->disableOriginalConstructor() - ->getMock(); - - $defaultIndex->expects($this->any()) - ->method('getName') - ->will($this->returnValue($this->defaultIndexName)); - - $this->indexManager = new IndexManager($this->indexesByName, $defaultIndex); - } - - public function testGetAllIndexes() - { - $this->assertEquals($this->indexesByName, $this->indexManager->getAllIndexes()); - } - - public function testGetIndex() - { - $this->assertEquals($this->indexesByName['index1'], $this->indexManager->getIndex('index1')); - $this->assertEquals($this->indexesByName['index2'], $this->indexManager->getIndex('index2')); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testGetIndexShouldThrowExceptionForInvalidName() - { - $this->indexManager->getIndex('index3'); - } - - public function testGetDefaultIndex() - { - $this->assertEquals('test2', $this->indexManager->getIndex()); - $this->assertEquals('test2', $this->indexManager->getDefaultIndex()); - } -} diff --git a/Tests/Integration/MappingTest.php b/Tests/Integration/MappingTest.php new file mode 100644 index 0000000..be134ed --- /dev/null +++ b/Tests/Integration/MappingTest.php @@ -0,0 +1,17 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + + +namespace FOS\ElasticaBundle\Tests\Integration; + + +class MappingTest { + +} \ No newline at end of file diff --git a/Tests/Persister/ObjectPersisterTest.php b/Tests/Persister/ObjectPersisterTest.php index 497c286..77a8809 100644 --- a/Tests/Persister/ObjectPersisterTest.php +++ b/Tests/Persister/ObjectPersisterTest.php @@ -47,10 +47,7 @@ class ObjectPersisterTest extends \PHPUnit_Framework_TestCase ->disableOriginalConstructor() ->getMock(); $typeMock->expects($this->once()) - ->method('deleteById') - ->with($this->equalTo(123)); - $typeMock->expects($this->once()) - ->method('addDocument'); + ->method('updateDocuments'); $fields = array('name' => array()); @@ -91,7 +88,7 @@ class ObjectPersisterTest extends \PHPUnit_Framework_TestCase $typeMock->expects($this->never()) ->method('deleteById'); $typeMock->expects($this->once()) - ->method('addDocument'); + ->method('addDocuments'); $fields = array('name' => array()); @@ -130,7 +127,7 @@ class ObjectPersisterTest extends \PHPUnit_Framework_TestCase ->disableOriginalConstructor() ->getMock(); $typeMock->expects($this->once()) - ->method('deleteById'); + ->method('deleteDocuments'); $typeMock->expects($this->never()) ->method('addDocument'); diff --git a/Tests/Persister/ObjectSerializerPersisterTest.php b/Tests/Persister/ObjectSerializerPersisterTest.php index aae3a64..914b5dd 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; @@ -42,10 +40,7 @@ class ObjectSerializerPersisterTest extends \PHPUnit_Framework_TestCase ->disableOriginalConstructor() ->getMock(); $typeMock->expects($this->once()) - ->method('deleteById') - ->with($this->equalTo(123)); - $typeMock->expects($this->once()) - ->method('addDocument'); + ->method('updateDocuments'); $serializerMock = $this->getMockBuilder('FOS\ElasticaBundle\Serializer\Callback')->getMock(); $serializerMock->expects($this->once())->method('serialize'); @@ -65,7 +60,7 @@ class ObjectSerializerPersisterTest extends \PHPUnit_Framework_TestCase $typeMock->expects($this->never()) ->method('deleteById'); $typeMock->expects($this->once()) - ->method('addDocument'); + ->method('addDocuments'); $serializerMock = $this->getMockBuilder('FOS\ElasticaBundle\Serializer\Callback')->getMock(); $serializerMock->expects($this->once())->method('serialize'); @@ -83,7 +78,7 @@ class ObjectSerializerPersisterTest extends \PHPUnit_Framework_TestCase ->disableOriginalConstructor() ->getMock(); $typeMock->expects($this->once()) - ->method('deleteById'); + ->method('deleteDocuments'); $typeMock->expects($this->never()) ->method('addDocument'); diff --git a/Tests/Provider/IndexableTest.php b/Tests/Provider/IndexableTest.php new file mode 100644 index 0000000..6ef5669 --- /dev/null +++ b/Tests/Provider/IndexableTest.php @@ -0,0 +1,114 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Tests\Provider; + +use FOS\ElasticaBundle\Provider\Indexable; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; + +class IndexableTest extends \PHPUnit_Framework_TestCase +{ + public $container; + + public function testIndexableUnknown() + { + $indexable = new Indexable(array(), $this->container); + $index = $indexable->isObjectIndexable('index', 'type', new Entity); + + $this->assertTrue($index); + } + + /** + * @dataProvider provideIsIndexableCallbacks + */ + public function testValidIndexableCallbacks($callback, $return) + { + $indexable = new Indexable(array( + 'index/type' => $callback + ), $this->container); + $index = $indexable->isObjectIndexable('index', 'type', new Entity); + + $this->assertEquals($return, $index); + } + + /** + * @dataProvider provideInvalidIsIndexableCallbacks + * @expectedException \InvalidArgumentException + */ + public function testInvalidIsIndexableCallbacks($callback) + { + $indexable = new Indexable(array( + 'index/type' => $callback + ), $this->container); + $indexable->isObjectIndexable('index', 'type', new Entity); + } + + public function provideInvalidIsIndexableCallbacks() + { + return array( + array('nonexistentEntityMethod'), + array(array(new IndexableDecider(), 'internalMethod')), + array(42), + array('entity.getIsIndexable() && nonexistentEntityFunction()'), + ); + } + + public function provideIsIndexableCallbacks() + { + return array( + array('isIndexable', false), + array(array(new IndexableDecider(), 'isIndexable'), true), + array(array('@indexableService', 'isIndexable'), 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), + ); + } + + protected function setUp() + { + $this->container = $this->getMockBuilder('Symfony\\Component\\DependencyInjection\\ContainerInterface') + ->getMock(); + + $this->container->expects($this->any()) + ->method('get') + ->with('indexableService') + ->will($this->returnValue(new IndexableDecider())); + } +} + +class Entity +{ + public $property = 'abc'; + + public function isIndexable() + { + return false; + } + + public function maybeIndex() + { + return true; + } +} + +class IndexableDecider +{ + public function isIndexable(Entity $entity) + { + return !$entity->isIndexable(); + } + + protected function internalMethod() + { + } +} diff --git a/Tests/Transformer/ElasticaToModelTransformerCollectionTest.php b/Tests/Transformer/ElasticaToModelTransformerCollectionTest.php index eb4d8e4..c3fc323 100644 --- a/Tests/Transformer/ElasticaToModelTransformerCollectionTest.php +++ b/Tests/Transformer/ElasticaToModelTransformerCollectionTest.php @@ -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 646d1a4..1fa6a8e 100644 --- a/Tests/Transformer/ModelToElasticaAutoTransformerTest.php +++ b/Tests/Transformer/ModelToElasticaAutoTransformerTest.php @@ -107,6 +107,11 @@ class POPO return array('foo' => 'foo', 'bar' => 'foo', 'id' => 1); } + public function getNestedObject() + { + return array('key1' => (object)array('id' => 1, 'key1sub1' => 'value1sub1', 'key1sub2' => 'value1sub2')); + } + public function getUpper() { return (object) array('id' => 'parent', 'name' => 'a random name'); @@ -296,6 +301,51 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase ), $data['obj']); } + public function testObjectsMappingOfAtLeastOneAutoMappedObjectAndAtLeastOneManuallyMappedObject() + { + $transformer = $this->getTransformer(); + $document = $transformer->transform( + new POPO(), + array( + 'obj' => array('type' => 'object', 'properties' => array()), + 'nestedObject' => array( + 'type' => 'object', + 'properties' => array( + 'key1sub1' => array( + 'type' => 'string', + 'properties' => array() + ), + 'key1sub2' => array( + 'type' => 'string', + 'properties' => array() + ) + ) + ) + ) + ); + $data = $document->getData(); + + $this->assertTrue(array_key_exists('obj', $data)); + $this->assertTrue(array_key_exists('nestedObject', $data)); + $this->assertInternalType('array', $data['obj']); + $this->assertInternalType('array', $data['nestedObject']); + $this->assertEquals( + array( + 'foo' => 'foo', + 'bar' => 'foo', + 'id' => 1 + ), + $data['obj'] + ); + $this->assertEquals( + array( + 'key1sub1' => 'value1sub1', + 'key1sub2' => 'value1sub2' + ), + $data['nestedObject'][0] + ); + } + public function testParentMapping() { $transformer = $this->getTransformer(); diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php deleted file mode 100644 index 30fb165..0000000 --- a/Tests/bootstrap.php +++ /dev/null @@ -1,19 +0,0 @@ -propertyAccessor->getValue($object, $key); - if (isset($mapping['type']) && in_array($mapping['type'], array('nested', 'object')) && isset($mapping['properties'])) { + 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. */ @@ -141,7 +141,7 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf }; if (is_array($value) || $value instanceof \Traversable || $value instanceof \ArrayAccess) { - $value = is_array($value) ? $value : iterator_to_array($value); + $value = is_array($value) ? $value : iterator_to_array($value, false); array_walk_recursive($value, $normalizeValue); } else { $normalizeValue($value); diff --git a/Transformer/ModelToElasticaIdentifierTransformer.php b/Transformer/ModelToElasticaIdentifierTransformer.php index 654850f..7cf97e6 100644 --- a/Transformer/ModelToElasticaIdentifierTransformer.php +++ b/Transformer/ModelToElasticaIdentifierTransformer.php @@ -21,6 +21,7 @@ class ModelToElasticaIdentifierTransformer extends ModelToElasticaAutoTransforme public function transform($object, array $fields) { $identifier = $this->propertyAccessor->getValue($object, $this->options['identifier']); + return new Document($identifier); } } diff --git a/composer.json b/composer.json index 8f22393..833621a 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,8 @@ "homepage": "https://github.com/FriendsOfSymfony/FOSElasticaBundle", "license": "MIT", "authors": [ - { "name": "Thibault Duplessis", "email": "thibault.duplessis@gmail.com" }, + { "name": "FriendsOfSymfony Community", "homepage": "https://github.com/FriendsOfSymfony/FOSElasticaBundle/contributors" }, + { "name": "Tim Nagel", "email": "tim@nagel.com.au" }, { "name": "Richard Miller", "email": "richard.miller@limethinking.co.uk" }, { "name": "Jeremy Mikola", "email": "jmikola@gmail.com" } ], @@ -16,32 +17,36 @@ "symfony/console": "~2.1", "symfony/form": "~2.1", "symfony/property-access": "~2.2", - "ruflin/elastica": ">=0.20, <1.1-dev", + "ruflin/elastica": ">=0.90.10.0, <1.4-dev", "psr/log": "~1.0" }, "require-dev":{ - "doctrine/orm": ">=2.2,<2.5-dev", - "doctrine/mongodb-odm": "1.0.*@dev", + "doctrine/orm": "~2.4", + "doctrine/doctrine-bundle": "~1.2@beta", + "jms/serializer-bundle": "@stable", + "phpunit/phpunit": "~4.1", "propel/propel1": "1.6.*", "pagerfanta/pagerfanta": "1.0.*@dev", - "knplabs/knp-components": "1.2.*", - "symfony/expression-language" : "2.4.*@dev" + "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,<2.5-dev", + "doctrine/orm": "~2.4", "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.*@dev" + "knplabs/knp-components": "~1.2", + "symfony/expression-language" : "~2.4" }, "autoload": { - "psr-0": { "FOS\\ElasticaBundle": "" } + "psr-4": { "FOS\\ElasticaBundle\\": "" } }, - "target-dir": "FOS/ElasticaBundle", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "3.1.x-dev" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 005cfd8..799d5bf 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,6 @@ - + ./Tests