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 72632de..fbb22d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,36 @@ language: php +cache: + directories: + - $HOME/.composer/cache + 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.*' 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 9d74640..57ca45c 100644 --- a/CHANGELOG-3.0.md +++ b/CHANGELOG-3.0.md @@ -12,7 +12,54 @@ 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.9 (2015-03-12) + + * Fix a bug in the BC layer of the type configuration for empty configs + * Fix the service definition for the Doctrine listener when the logger is not enabled + +* 3.0.8 (2014-01-31) + + * Fixed handling of empty indexes #760 + * Added support for `connectionStrategy` Elastica configuration #732 + * Allow Elastica 1.4 + +* 3.0.7 (2015-01-21) + + * Fixed the indexing of parent/child relations, broken since 3.0 #774 + * Fixed multi_field properties not being normalised #769 + +* 3.0.6 (2015-01-04) + + * Removed unused public image asset for the web development toolbar #742 + * Fixed is_indexable_callback BC code to support array notation #761 + * Fixed debug_logger for type providers #724 + * Clean the OM if we filter away the entire batch #737 + +* 3.0.0-ALPHA6 + + * Moved `is_indexable_callback` from the listener properties to a type property called + `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 diff --git a/CHANGELOG-3.1.md b/CHANGELOG-3.1.md new file mode 100644 index 0000000..3ca3c77 --- /dev/null +++ b/CHANGELOG-3.1.md @@ -0,0 +1,61 @@ +CHANGELOG for 3.1.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 3.1 versions. + +To get the diff for a specific change, go to +https://github.com/FriendsOfSymfony/FOSElasticaBundle/commit/XXX where XXX is +the commit hash. To get the diff between two versions, go to +https://github.com/FriendsOfSymfony/FOSElasticaBundle/compare/v3.0.4...v3.1.0 + +* 3.1.3 (2015-04-02) + + * Fix Symfony 2.3 compatibility + +* 3.1.2 (2015-03-27) + + * Fix the previous release + +* 3.1.1 (2015-03-27) + + * Fix PopulateCommand trying to set formats for ProgressBar in Symfony < 2.5 + * Fix Provider implementations that depend on a batch size from going into + infinite loops + +* 3.1.0 (2015-03-18) + + * BC BREAK: `Doctrine\Listener#scheduleForDeletion` access changed to private. + * BC BREAK: `ObjectPersisterInterface` gains the method `handlesObject` that + returns a boolean value if it will handle a given object or not. + * BC BREAK: Removed `Doctrine\Listener#getSubscribedEvents`. The container + configuration now configures tags with the methods to call to avoid loading + this class on every request where doctrine is active. #729 + * BC BREAK: Added methods for retrieving aggregations when paginating results. + The `PaginationAdapterInterface` has a new method, `getAggregations`. #726 + * Added ability to configure `date_detection`, `numeric_detection` and + `dynamic_date_formats` for types. #753 + * New event `POST_TRANSFORM` which allows developers to add custom properties to + Elastica Documents for indexing. + * When available, the `fos:elastica:populate` command will now use the + ProgressBar helper instead of outputting strings. You can use verbosity + controls on the command to output additional information like memory + usage, runtime and estimated time. + * Added new option `property_path` to a type property definition to allow + customisation of the property path used to retrieve data from objects. + Setting `property_path` to `false` will configure the Transformer to ignore + that property while transforming. Combined with the above POST_TRANSFORM event + developers can now create calculated dynamic properties on Elastica documents + for indexing. #794 + * Fixed a case where ProgressCommand would always ignore errors regardless of + --ignore-errors being passed or not. + * Added a `SliceFetcher` abstraction for Doctrine providers that get more + information about the previous slice allowing for optimising queries during + population. #725 + * New events `PRE_INDEX_POPULATE`, `POST_INDEX_POPULATE`, `PRE_TYPE_POPULATE` and + `POST_TYPE_POPULATE` allow for monitoring when an index is about to be or has + just been populated. #744 + * New events `PRE_INDEX_RESET`, `POST_INDEX_RESET`, `PRE_TYPE_RESET` and + `POST_TYPE_RESET` are run before and after operations that will reset an + index. #744 + * Added indexable callback support for the __invoke method of a service. #823 diff --git a/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 af5fd5d..42af355 100644 --- a/Command/PopulateCommand.php +++ b/Command/PopulateCommand.php @@ -2,6 +2,8 @@ namespace FOS\ElasticaBundle\Command; +use FOS\ElasticaBundle\Event\IndexPopulateEvent; +use FOS\ElasticaBundle\Event\TypePopulateEvent; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Helper\DialogHelper; use Symfony\Component\Console\Input\InputOption; @@ -10,18 +12,28 @@ use Symfony\Component\Console\Output\OutputInterface; use FOS\ElasticaBundle\IndexManager; use FOS\ElasticaBundle\Provider\ProviderRegistry; use FOS\ElasticaBundle\Resetter; -use FOS\ElasticaBundle\Provider\ProviderInterface; +use Symfony\Component\Console\Helper\ProgressBar; /** - * Populate the search index + * Populate the search index. */ class PopulateCommand extends ContainerAwareCommand { + /** + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + private $dispatcher; + /** * @var IndexManager */ private $indexManager; + /** + * @var ProgressClosureBuilder + */ + private $progressClosureBuilder; + /** * @var ProviderRegistry */ @@ -46,31 +58,46 @@ class PopulateCommand extends ContainerAwareCommand ->addOption('sleep', null, InputOption::VALUE_REQUIRED, 'Sleep time between persisting iterations (microseconds)', 0) ->addOption('batch-size', null, InputOption::VALUE_REQUIRED, 'Index packet size (overrides provider config option)') ->addOption('ignore-errors', null, InputOption::VALUE_NONE, 'Do not stop on errors') + ->addOption('no-overwrite-format', null, InputOption::VALUE_NONE, 'Prevent this command from overwriting ProgressBar\'s formats') ->setDescription('Populates search indexes from providers') ; } /** - * @see Symfony\Component\Console\Command\Command::initialize() + * {@inheritDoc} */ protected function initialize(InputInterface $input, OutputInterface $output) { + $this->dispatcher = $this->getContainer()->get('event_dispatcher'); $this->indexManager = $this->getContainer()->get('fos_elastica.index_manager'); $this->providerRegistry = $this->getContainer()->get('fos_elastica.provider_registry'); $this->resetter = $this->getContainer()->get('fos_elastica.resetter'); + $this->progressClosureBuilder = new ProgressClosureBuilder(); + + if (!$input->getOption('no-overwrite-format') && class_exists('Symfony\\Component\\Console\\Helper\\ProgressBar')) { + ProgressBar::setFormatDefinition('normal', " %current%/%max% [%bar%] %percent:3s%%\n%message%"); + ProgressBar::setFormatDefinition('verbose', " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%\n%message%"); + ProgressBar::setFormatDefinition('very_verbose', " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%\n%message%"); + ProgressBar::setFormatDefinition('debug', " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%\n%message%"); + } } /** - * @see Symfony\Component\Console\Command\Command::execute() + * {@inheritDoc} */ protected function execute(InputInterface $input, OutputInterface $output) { - $index = $input->getOption('index'); - $type = $input->getOption('type'); - $reset = !$input->getOption('no-reset'); - $options = $input->getOptions(); - - $options['ignore-errors'] = $input->hasOption('ignore-errors'); + $index = $input->getOption('index'); + $type = $input->getOption('type'); + $reset = !$input->getOption('no-reset'); + $options = array( + 'ignore_errors' => $input->getOption('ignore-errors'), + 'offset' => $input->getOption('offset'), + 'sleep' => $input->getOption('sleep') + ); + if ($input->getOption('batch-size')) { + $options['batch_size'] = (int) $input->getOption('batch-size'); + } if ($input->isInteractive() && $reset && $input->getOption('offset')) { /** @var DialogHelper $dialog */ @@ -109,25 +136,22 @@ class PopulateCommand extends ContainerAwareCommand */ private function populateIndex(OutputInterface $output, $index, $reset, $options) { - if ($reset) { + $event = new IndexPopulateEvent($index, $reset, $options); + $this->dispatcher->dispatch(IndexPopulateEvent::PRE_INDEX_POPULATE, $event); + + if ($event->isReset()) { $output->writeln(sprintf('Resetting %s', $index)); - $this->resetter->resetIndex($index); + $this->resetter->resetIndex($index, true); } - /** @var $providers ProviderInterface[] */ - $providers = $this->providerRegistry->getIndexProviders($index); - - foreach ($providers as $type => $provider) { - $loggerClosure = function($message) use ($output, $index, $type) { - $output->writeln(sprintf('Populating %s/%s, %s', $index, $type, $message)); - }; - - $provider->populate($loggerClosure, $options); + $types = array_keys($this->providerRegistry->getIndexProviders($index)); + foreach ($types as $type) { + $this->populateIndexType($output, $index, $type, false, $event->getOptions()); } - $output->writeln(sprintf('Refreshing %s', $index)); - $this->resetter->postPopulate($index); - $this->indexManager->getIndex($index)->refresh(); + $this->dispatcher->dispatch(IndexPopulateEvent::POST_INDEX_POPULATE, $event); + + $this->refreshIndex($output, $index); } /** @@ -141,17 +165,35 @@ class PopulateCommand extends ContainerAwareCommand */ private function populateIndexType(OutputInterface $output, $index, $type, $reset, $options) { - if ($reset) { + $event = new TypePopulateEvent($index, $type, $reset, $options); + $this->dispatcher->dispatch(TypePopulateEvent::PRE_TYPE_POPULATE, $event); + + if ($event->isReset()) { $output->writeln(sprintf('Resetting %s/%s', $index, $type)); $this->resetter->resetIndexType($index, $type); } - $loggerClosure = function($message) use ($output, $index, $type) { - $output->writeln(sprintf('Populating %s/%s, %s', $index, $type, $message)); - }; - $provider = $this->providerRegistry->getProvider($index, $type); - $provider->populate($loggerClosure, $options); + $loggerClosure = $this->progressClosureBuilder->build($output, 'Populating', $index, $type); + $provider->populate($loggerClosure, $event->getOptions()); + + $this->dispatcher->dispatch(TypePopulateEvent::POST_TYPE_POPULATE, $event); + + $this->refreshIndex($output, $index, false); + } + + /** + * Refreshes an index. + * + * @param OutputInterface $output + * @param string $index + * @param bool $postPopulate + */ + private function refreshIndex(OutputInterface $output, $index, $postPopulate = true) + { + if ($postPopulate) { + $this->resetter->postPopulate($index); + } $output->writeln(sprintf('Refreshing %s', $index)); $this->indexManager->getIndex($index)->refresh(); diff --git a/Command/ProgressClosureBuilder.php b/Command/ProgressClosureBuilder.php new file mode 100644 index 0000000..f244bc3 --- /dev/null +++ b/Command/ProgressClosureBuilder.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Command; + +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\OutputInterface; + +class ProgressClosureBuilder +{ + /** + * Builds a loggerClosure to be called from inside the Provider to update the command + * line. + * + * @param OutputInterface $output + * @param string $action + * @param string $index + * @param string $type + * + * @return callable + */ + public function build(OutputInterface $output, $action, $index, $type) + { + if (!class_exists('Symfony\Component\Console\Helper\ProgressBar') || + !is_callable(array('Symfony\Component\Console\Helper\ProgressBar', 'getProgress'))) { + return $this->buildLegacy($output, $action, $index, $type); + } + + $progress = null; + + return function ($increment, $totalObjects, $message = null) use (&$progress, $output, $action, $index, $type) { + if (null === $progress) { + $progress = new ProgressBar($output, $totalObjects); + $progress->start(); + } + + if (null !== $message) { + $progress->clear(); + $output->writeln(sprintf('%s %s', $action, $message)); + $progress->display(); + } + + $progress->setMessage(sprintf('%s %s/%s', $action, $index, $type)); + $progress->advance($increment); + }; + } + + /** + * Builds a legacy closure that outputs lines for each step. Used in cases + * where the ProgressBar component doesnt exist or does not have the correct + * methods to support what we need. + * + * @param OutputInterface $output + * @param string $action + * @param string $index + * @param string $type + * + * @return callable + */ + private function buildLegacy(OutputInterface $output, $action, $index, $type) + { + $lastStep = null; + $current = 0; + + return function ($increment, $totalObjects, $message = null) use ($output, $action, $index, $type, &$lastStep, &$current) { + if ($current + $increment > $totalObjects) { + $increment = $totalObjects - $current; + } + + if (null !== $message) { + $output->writeln(sprintf('%s %s', $action, $message)); + } + + $currentTime = microtime(true); + $timeDifference = $currentTime - $lastStep; + $objectsPerSecond = $lastStep ? ($increment / $timeDifference) : $increment; + $lastStep = $currentTime; + $current += $increment; + $percent = 100 * $current / $totalObjects; + + $output->writeln(sprintf( + '%s %s/%s %0.1f%% (%d/%d), %d objects/s (RAM: current=%uMo peak=%uMo)', + $action, + $index, + $type, + $percent, + $current, + $totalObjects, + $objectsPerSecond, + round(memory_get_usage() / (1024 * 1024)), + round(memory_get_peak_usage() / (1024 * 1024)) + )); + }; + } +} diff --git a/Command/ResetCommand.php b/Command/ResetCommand.php index 06cfe48..85c5483 100755 --- a/Command/ResetCommand.php +++ b/Command/ResetCommand.php @@ -10,7 +10,7 @@ use FOS\ElasticaBundle\IndexManager; use FOS\ElasticaBundle\Resetter; /** - * Reset search indexes + * Reset search indexes. */ class ResetCommand extends ContainerAwareCommand { @@ -33,6 +33,7 @@ class ResetCommand extends ContainerAwareCommand ->setName('fos:elastica:reset') ->addOption('index', null, InputOption::VALUE_OPTIONAL, 'The index to reset') ->addOption('type', null, InputOption::VALUE_OPTIONAL, 'The type to reset') + ->addOption('force', null, InputOption::VALUE_NONE, 'Force index deletion if same name as alias') ->setDescription('Reset search indexes') ; } @@ -51,8 +52,9 @@ class ResetCommand extends ContainerAwareCommand */ protected function execute(InputInterface $input, OutputInterface $output) { - $index = $input->getOption('index'); - $type = $input->getOption('type'); + $index = $input->getOption('index'); + $type = $input->getOption('type'); + $force = (bool) $input->getOption('force'); if (null === $index && null !== $type) { throw new \InvalidArgumentException('Cannot specify type option without an index.'); @@ -69,7 +71,7 @@ class ResetCommand extends ContainerAwareCommand foreach ($indexes as $index) { $output->writeln(sprintf('Resetting %s', $index)); - $this->resetter->resetIndex($index); + $this->resetter->resetIndex($index, false, $force); } } } diff --git a/Command/SearchCommand.php b/Command/SearchCommand.php index ec7cfb7..11183de 100644 --- a/Command/SearchCommand.php +++ b/Command/SearchCommand.php @@ -11,7 +11,7 @@ use Elastica\Query; use Elastica\Result; /** - * Searches a type + * Searches a type. */ class SearchCommand extends ContainerAwareCommand { diff --git a/Configuration/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..749b10d --- /dev/null +++ b/Configuration/IndexConfig.php @@ -0,0 +1,124 @@ + + * + * 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..742df1b --- /dev/null +++ b/Configuration/ManagerInterface.php @@ -0,0 +1,44 @@ + + * + * 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..1d046c0 100644 --- a/Configuration/Search.php +++ b/Configuration/Search.php @@ -1,16 +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; +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..25e6f86 --- /dev/null +++ b/Configuration/Source/ContainerSource.php @@ -0,0 +1,80 @@ + + * + * 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 = $this->getTypes($config); + $index = new IndexConfig($config['name'], $types, array( + 'elasticSearchName' => $config['elasticsearch_name'], + 'settings' => $config['settings'], + 'useAlias' => $config['use_alias'], + )); + + $indexes[$config['name']] = $index; + } + + return $indexes; + } + + /** + * Builds TypeConfig objects for each type. + * + * @param array $config + * + * @return array + */ + protected function getTypes($config) + { + $types = array(); + + if (isset($config['types'])) { + foreach ($config['types'] as $typeConfig) { + $types[$typeConfig['name']] = new TypeConfig( + $typeConfig['name'], + $typeConfig['mapping'], + $typeConfig['config'] + ); + // TODO: handle prototypes.. + } + } + + return $types; + } +} diff --git a/Configuration/Source/SourceInterface.php b/Configuration/Source/SourceInterface.php new file mode 100644 index 0000000..05a64d0 --- /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..a46cd34 --- /dev/null +++ b/Configuration/TypeConfig.php @@ -0,0 +1,113 @@ + + * + * 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 bool|null + */ + public function getDateDetection() + { + return $this->getConfig('date_detection'); + } + + /** + * @return array + */ + public function getDynamicDateFormats() + { + return $this->getConfig('dynamic_date_formats'); + } + + /** + * @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 bool|null + */ + public function getNumericDetection() + { + return $this->getConfig('numeric_detection'); + } + + /** + * @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..92a2489 --- /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/Compiler/RegisterProvidersPass.php b/DependencyInjection/Compiler/RegisterProvidersPass.php index c6c9e6e..4fd25b0 100644 --- a/DependencyInjection/Compiler/RegisterProvidersPass.php +++ b/DependencyInjection/Compiler/RegisterProvidersPass.php @@ -55,6 +55,7 @@ class RegisterProvidersPass implements CompilerPassInterface * Returns whether the class implements ProviderInterface. * * @param string $class + * * @return boolean */ private function isProviderImplementation($class) diff --git a/DependencyInjection/Compiler/TransformerPass.php b/DependencyInjection/Compiler/TransformerPass.php index 4281d0b..596c732 100644 --- a/DependencyInjection/Compiler/TransformerPass.php +++ b/DependencyInjection/Compiler/TransformerPass.php @@ -31,7 +31,7 @@ class TransformerPass implements CompilerPassInterface throw new InvalidArgumentException('The Transformer must have both a type and an index defined.'); } - $transformers[$tag['index']][$tag['type']]= new Reference($id); + $transformers[$tag['index']][$tag['type']] = new Reference($id); } } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index d0ec09f..1391eaa 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -15,20 +15,22 @@ class Configuration implements ConfigurationInterface */ 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, $debug) + 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() { @@ -61,7 +63,7 @@ class Configuration implements ConfigurationInterface } /** - * Adds the configuration for the "clients" key + * Adds the configuration for the "clients" key. */ private function addClientsSection(ArrayNodeDefinition $rootNode) { @@ -72,38 +74,52 @@ 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'])) || isset($v['url']); }) - ->then(function($v) { - return array( - 'servers' => array( - array( - 'host' => isset($v['host']) ? $v['host'] : null, - 'port' => isset($v['port']) ? $v['port'] : null, - 'url' => isset($v['url']) ? $v['url'] : null, - 'logger' => isset($v['logger']) ? $v['logger'] : null, - 'headers' => isset($v['headers']) ? $v['headers'] : null, - 'timeout' => isset($v['timeout']) ? $v['timeout'] : null, - ) - ) - ); - }) + ->ifTrue(function ($v) { return isset($v['servers']); }) + ->then(function ($v) { + $v['connections'] = $v['servers']; + unset($v['servers']); + + return $v; + }) + ->end() + // Elastica names its properties with camel case, support both + ->beforeNormalization() + ->ifTrue(function ($v) { return isset($v['connection_strategy']); }) + ->then(function ($v) { + $v['connectionStrategy'] = $v['connection_strategy']; + unset($v['connection_strategy']); + + return $v; + }) + ->end() + // If there is no connections array key defined, assume a single connection. + ->beforeNormalization() + ->ifTrue(function ($v) { return is_array($v) && !array_key_exists('connections', $v); }) + ->then(function ($v) { + return array( + 'connections' => array($v), + ); + }) ->end() ->children() - ->arrayNode('servers') + ->arrayNode('connections') + ->requiresAtLeastOneElement() ->prototype('array') ->fixXmlConfig('header') ->children() ->scalarNode('url') ->validate() - ->ifTrue(function($url) { return substr($url, -1) !== '/'; }) - ->then(function($url) { return $url.'/'; }) + ->ifTrue(function ($url) { return $url && substr($url, -1) !== '/'; }) + ->then(function ($url) { return $url.'/'; }) ->end() ->end() ->scalarNode('host')->end() ->scalarNode('port')->end() + ->scalarNode('proxy')->end() ->scalarNode('logger') - ->defaultValue(($this->debug) ? 'fos_elastica.logger' : false) + ->defaultValue($this->debug ? 'fos_elastica.logger' : false) ->treatNullLike('fos_elastica.logger') ->treatTrueLike('fos_elastica.logger') ->end() @@ -111,12 +127,14 @@ class Configuration implements ConfigurationInterface ->useAttributeAsKey('name') ->prototype('scalar')->end() ->end() + ->scalarNode('transport')->end() ->scalarNode('timeout')->end() ->end() ->end() ->end() ->scalarNode('timeout')->end() ->scalarNode('headers')->end() + ->scalarNode('connectionStrategy')->defaultValue('Simple')->end() ->end() ->end() ->end() @@ -125,7 +143,7 @@ class Configuration implements ConfigurationInterface } /** - * Adds the configuration for the "indexes" key + * Adds the configuration for the "indexes" key. */ private function addIndexesSection(ArrayNodeDefinition $rootNode) { @@ -174,14 +192,75 @@ class Configuration implements ConfigurationInterface ->useAttributeAsKey('name') ->prototype('array') ->treatNullLike(array()) + ->beforeNormalization() + ->ifNull() + ->thenEmptyArray() + ->end() + // BC - Renaming 'mappings' node to 'properties' + ->beforeNormalization() + ->ifTrue(function ($v) { return array_key_exists('mappings', $v); }) + ->then(function ($v) { + $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) { + $callback = $v['persistence']['listener']['is_indexable_callback']; + + if (is_array($callback)) { + list($class) = $callback + array(null); + + if ($class[0] !== '@' && is_string($class) && !class_exists($class)) { + $callback[0] = '@'.$class; + } + } + + $v['indexable_callback'] = $callback; + unset($v['persistence']['listener']['is_indexable_callback']); + + return $v; + }) + ->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() + ->booleanNode('date_detection')->end() + ->arrayNode('dynamic_date_formats')->prototype('scalar')->end()->end() ->scalarNode('index_analyzer')->end() + ->booleanNode('numeric_detection')->end() ->scalarNode('search_analyzer')->end() + ->variableNode('indexable_callback')->end() ->append($this->getPersistenceNode()) ->append($this->getSerializerNode()) ->end() ->append($this->getIdNode()) - ->append($this->getMappingsNode()) + ->append($this->getPropertiesNode()) ->append($this->getDynamicTemplateNode()) ->append($this->getSourceNode()) ->append($this->getBoostNode()) @@ -197,35 +276,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() - ->always() - ->then(function($v) { - foreach (array('fields','properties') as $prop) { - if (isset($v[$prop]) && empty($v[$prop])) { - unset($v[$prop]); - } - } - - return $v; - }) - ->end() - ->treatNullLike(array()) - ->addDefaultsIfNotSet() - ->children(); - - $this->addFieldConfig($childrenNode, $nestings); + ->prototype('variable') + ->treatNullLike(array()); return $node; } @@ -239,221 +300,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('tree')->end() - ->scalarNode('precision')->end() - ->scalarNode('tree_levels')->end() - ->scalarNode('geohash')->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') - ->validate() - ->always() - ->then(function($v) { - foreach (array('fields','properties') as $prop) { - if (isset($v[$prop]) && empty($v[$prop])) { - unset($v[$prop]); - } - } - - return $v; - }) - ->end() - ->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; } /** @@ -493,7 +359,7 @@ class Configuration implements ConfigurationInterface ->end() ->scalarNode('compress')->end() ->scalarNode('compress_threshold')->end() - ->scalarNode('enabled')->end() + ->scalarNode('enabled')->defaultTrue()->end() ->end() ; @@ -556,7 +422,7 @@ class Configuration implements ConfigurationInterface } /** - * Returns the array node used for "_all" + * Returns the array node used for "_all". */ protected function getAllNode() { @@ -575,7 +441,7 @@ class Configuration implements ConfigurationInterface } /** - * Returns the array node used for "_timestamp" + * Returns the array node used for "_timestamp". */ protected function getTimestampNode() { @@ -596,7 +462,7 @@ class Configuration implements ConfigurationInterface } /** - * Returns the array node used for "_ttl" + * Returns the array node used for "_ttl". */ protected function getTtlNode() { @@ -625,9 +491,9 @@ class Configuration implements ConfigurationInterface $node ->validate() - ->ifTrue(function($v) { return isset($v['driver']) && 'propel' === $v['driver'] && isset($v['listener']); }) + ->ifTrue(function ($v) { return isset($v['driver']) && 'propel' === $v['driver'] && isset($v['listener']); }) ->thenInvalid('Propel doesn\'t support listeners') - ->ifTrue(function($v) { return isset($v['driver']) && 'propel' === $v['driver'] && isset($v['repository']); }) + ->ifTrue(function ($v) { return isset($v['driver']) && 'propel' === $v['driver'] && isset($v['repository']); }) ->thenInvalid('Propel doesn\'t support the "repository" parameter') ->end() ->children() @@ -642,9 +508,13 @@ class Configuration implements ConfigurationInterface ->scalarNode('identifier')->defaultValue('id')->end() ->arrayNode('provider') ->children() - ->scalarNode('query_builder_method')->defaultValue('createQueryBuilder')->end() ->scalarNode('batch_size')->defaultValue(100)->end() ->scalarNode('clear_object_manager')->defaultTrue()->end() + ->scalarNode('debug_logging') + ->defaultValue($this->debug) + ->treatNullLike(true) + ->end() + ->scalarNode('query_builder_method')->defaultValue('createQueryBuilder')->end() ->scalarNode('service')->end() ->end() ->end() @@ -653,6 +523,7 @@ class Configuration implements ConfigurationInterface ->scalarNode('insert')->defaultTrue()->end() ->scalarNode('update')->defaultTrue()->end() ->scalarNode('delete')->defaultTrue()->end() + ->scalarNode('flush')->defaultTrue()->end() ->booleanNode('immediate')->defaultFalse()->end() ->scalarNode('logger') ->defaultFalse() @@ -660,7 +531,6 @@ class Configuration implements ConfigurationInterface ->treatTrueLike('fos_elastica.logger') ->end() ->scalarNode('service')->end() - ->variableNode('is_indexable_callback')->defaultNull()->end() ->end() ->end() ->arrayNode('finder') diff --git a/DependencyInjection/FOSElasticaExtension.php b/DependencyInjection/FOSElasticaExtension.php index f58cd5b..ec45e25 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,15 +12,32 @@ 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')); @@ -31,7 +46,9 @@ class FOSElasticaExtension extends Extension return; } - $loader->load('config.xml'); + foreach (array('config', 'index', 'persister', 'provider', 'source', 'transformer') as $basename) { + $loader->load(sprintf('%s.xml', $basename)); + } if (empty($config['default_client'])) { $keys = array_keys($config['clients']); @@ -43,41 +60,53 @@ 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, $container->getParameter('kernel.debug')); + return new Configuration($container->getParameter('kernel.debug')); } /** * Loads the configured clients. * - * @param array $clients An array of clients configurations + * @param array $clients An array of clients configurations * @param ContainerBuilder $container A ContainerBuilder instance + * * @return array */ - 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))); } @@ -85,78 +114,75 @@ class FOSElasticaExtension extends Extension $container->setDefinition($clientId, $clientDef); - $clientIds[$name] = $clientId; + $this->clients[$name] = array( + 'id' => $clientId, + 'reference' => new Reference($clientId), + ); } - - return $clientIds; } /** * Loads the configured indexes. * - * @param array $indexes An array of indexes configurations + * @param array $indexes An array of indexes configurations * @param ContainerBuilder $container A ContainerBuilder instance - * @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(); - 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; - } + $indexableCallbacks = array(); - $clientId = $clientIdsByName[$clientName]; + foreach ($indexes as $name => $index) { $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; + + $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); } /** * Loads the configured index finders. * * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container - * @param string $name The index name - * @param string $indexId The index service identifier + * @param string $name The index name + * @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. @@ -167,167 +193,141 @@ 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->cleanUpMapping($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', + 'date_detection', + 'dynamic_date_formats', + 'numeric_detection', + ) 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 + * 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 array $typeConfig + * @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. */ @@ -340,55 +340,78 @@ 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); $serviceDef->replaceArgument(0, array( - 'identifier' => $typeConfig['identifier'] + 'identifier' => $typeConfig['identifier'], )); $container->setDefinition($serviceId, $serviceDef); 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)]; + $mapping = $this->indexConfigs[$indexName]['types'][$typeName]['mapping']; + $argument = $mapping['properties']; + if (isset($mapping['_parent'])) { + $argument['_parent'] = $mapping['_parent']; + } + $arguments[] = $argument; } + $serviceId = sprintf('fos_elastica.object_persister.%s.%s', $indexName, $typeName); $serviceDef = new DefinitionDecorator($abstractId); foreach ($arguments as $i => $argument) { @@ -400,31 +423,58 @@ 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. */ $providerId = sprintf('fos_elastica.provider.%s.%s', $indexName, $typeName); - $providerDef = new DefinitionDecorator('fos_elastica.provider.prototype.' . $typeConfig['driver']); + $providerDef = new DefinitionDecorator('fos_elastica.provider.prototype.'.$typeConfig['driver']); $providerDef->addTag('fos_elastica.provider', array('index' => $indexName, 'type' => $typeName)); $providerDef->replaceArgument(0, new Reference($objectPersisterId)); - $providerDef->replaceArgument(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. */ @@ -432,36 +482,39 @@ 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(2, $this->getDoctrineEvents($typeConfig)); - $listenerDef->replaceArgument(3, $typeConfig['identifier']); - if ($typeConfig['listener']['logger']) { - $listenerDef->replaceArgument(4, new Reference($typeConfig['listener']['logger'])); - } + $listenerDef->replaceArgument(2, array( + 'identifier' => $typeConfig['identifier'], + 'indexName' => $indexName, + 'typeName' => $typeName, + )); + $listenerDef->replaceArgument(3, $typeConfig['listener']['logger'] ? + new Reference($typeConfig['listener']['logger']) : + null + ); + $tagName = null; switch ($typeConfig['driver']) { - case 'orm': $listenerDef->addTag('doctrine.event_subscriber'); break; - case 'mongodb': $listenerDef->addTag('doctrine_mongodb.odm.event_subscriber'); break; + case 'orm': + $tagName = 'doctrine.event_listener'; + break; + case 'mongodb': + $tagName = 'doctrine_mongodb.odm.event_listener'; + break; } - if (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); - } + if (null !== $tagName) { + foreach ($this->getDoctrineEvents($typeConfig) as $event) { + $listenerDef->addTag($tagName, array('event' => $event)); } - - $listenerDef->addMethodCall('setIsIndexableCallback', array($callback)); } + $container->setDefinition($listenerId, $listenerDef); return $listenerId; } /** - * Map Elastica to Doctrine events for the current driver + * Map Elastica to Doctrine events for the current driver. */ private function getDoctrineEvents(array $typeConfig) { @@ -474,7 +527,6 @@ class FOSElasticaExtension extends Extension break; default: throw new InvalidArgumentException(sprintf('Cannot determine events for driver "%s"', $typeConfig['driver'])); - break; } $events = array(); @@ -482,7 +534,7 @@ class FOSElasticaExtension extends Extension 'insert' => array(constant($eventsClass.'::postPersist')), 'update' => array(constant($eventsClass.'::postUpdate')), 'delete' => array(constant($eventsClass.'::preRemove')), - 'flush' => array($typeConfig['listener']['immediate'] ? constant($eventsClass.'::preFlush') : constant($eventsClass.'::postFlush')) + 'flush' => array($typeConfig['listener']['immediate'] ? constant($eventsClass.'::preFlush') : constant($eventsClass.'::postFlush')), ); foreach ($eventMapping as $event => $doctrineEvents) { @@ -494,14 +546,26 @@ 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); } @@ -518,41 +582,63 @@ class FOSElasticaExtension extends Extension } /** - * Loads the index manager + * 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; @@ -569,18 +655,21 @@ class FOSElasticaExtension extends Extension $container->setAlias('fos_elastica.manager', sprintf('fos_elastica.manager.%s', $defaultManagerService)); } - protected function cleanUpMapping(&$mappings) + /** + * Returns a reference to a client given its configured name. + * + * @param string $clientName + * + * @return Reference + * + * @throws \InvalidArgumentException + */ + private function getClient($clientName) { - foreach ($mappings as &$fieldProperties) { - if (empty($fieldProperties['fields'])) { - unset($fieldProperties['fields']); - } else { - $this->cleanUpMapping($fieldProperties['fields']); - } - - if (!empty($fieldProperties['properties'])) { - $this->cleanUpMapping($fieldProperties['properties']); - } + 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..0263f42 100755 --- a/Doctrine/AbstractElasticaToModelTransformer.php +++ b/Doctrine/AbstractElasticaToModelTransformer.php @@ -2,32 +2,34 @@ namespace FOS\ElasticaBundle\Doctrine; +use Doctrine\Common\Persistence\ManagerRegistry; use FOS\ElasticaBundle\HybridResult; -use FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerInterface; +use FOS\ElasticaBundle\Transformer\AbstractElasticaToModelTransformer as BaseTransformer; use FOS\ElasticaBundle\Transformer\HighlightableModelInterface; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * Maps Elastica documents with Doctrine objects * This mapper assumes an exact match between - * elastica documents ids and doctrine object ids + * elastica documents ids and doctrine object ids. */ -abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTransformerInterface +abstract class AbstractElasticaToModelTransformer extends BaseTransformer { /** - * Manager registry + * Manager registry. + * + * @var ManagerRegistry */ protected $registry = null; /** - * Class of the model to map to the elastica documents + * Class of the model to map to the elastica documents. * * @var string */ protected $objectClass = null; /** - * Optional parameters + * Optional parameters. * * @var array */ @@ -39,20 +41,13 @@ abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTran ); /** - * PropertyAccessor instance + * Instantiates a new Mapper. * - * @var PropertyAccessorInterface - */ - protected $propertyAccessor; - - /** - * Instantiates a new Mapper - * - * @param object $registry + * @param ManagerRegistry $registry * @param string $objectClass - * @param array $options + * @param array $options */ - public function __construct($registry, $objectClass, array $options = array()) + public function __construct(ManagerRegistry $registry, $objectClass, array $options = array()) { $this->registry = $registry; $this->objectClass = $objectClass; @@ -69,22 +64,14 @@ abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTran return $this->objectClass; } - /** - * Set the PropertyAccessor - * - * @param PropertyAccessorInterface $propertyAccessor - */ - public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor) - { - $this->propertyAccessor = $propertyAccessor; - } - /** * Transforms an array of elastica objects into an array of - * model objects fetched from the doctrine repository + * model objects fetched from the doctrine repository. * * @param array $elasticaObjects of elastica objects + * * @throws \RuntimeException + * * @return array **/ public function transform(array $elasticaObjects) @@ -109,29 +96,31 @@ abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTran // sort objects in the order of ids $idPos = array_flip($ids); $identifier = $this->options['identifier']; - $propertyAccessor = $this->propertyAccessor; - usort($objects, function($a, $b) use ($idPos, $identifier, $propertyAccessor) - { - return $idPos[$propertyAccessor->getValue($a, $identifier)] > $idPos[$propertyAccessor->getValue($b, $identifier)]; - }); + usort($objects, $this->getSortingClosure($idPos, $identifier)); return $objects; } 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; } /** - * {@inheritdoc} + * {@inheritDoc} */ public function getIdentifierField() { @@ -139,11 +128,12 @@ abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTran } /** - * Fetches objects by theses identifier values + * Fetches objects by theses identifier values. + * + * @param array $identifierValues ids values + * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays * - * @param array $identifierValues ids values - * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays * @return array of objects or arrays */ - protected abstract function findByIdentifiers(array $identifierValues, $hydrate); + abstract protected function findByIdentifiers(array $identifierValues, $hydrate); } diff --git a/Doctrine/AbstractProvider.php b/Doctrine/AbstractProvider.php index b9ffda5..ec198a8 100644 --- a/Doctrine/AbstractProvider.php +++ b/Doctrine/AbstractProvider.php @@ -6,109 +6,62 @@ 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; +use Symfony\Component\OptionsResolver\OptionsResolver; abstract class AbstractProvider extends BaseAbstractProvider { + /** + * @var SliceFetcherInterface + */ + private $sliceFetcher; + + /** + * @var ManagerRegistry + */ protected $managerRegistry; /** * Constructor. * * @param ObjectPersisterInterface $objectPersister + * @param IndexableInterface $indexable * @param string $objectClass - * @param array $options + * @param array $baseOptions * @param ManagerRegistry $managerRegistry + * @param SliceFetcherInterface $sliceFetcher */ - public function __construct(ObjectPersisterInterface $objectPersister, $objectClass, array $options, $managerRegistry) - { - parent::__construct($objectPersister, $objectClass, array_merge(array( - 'clear_object_manager' => true, - 'debug_logging' => false, - 'ignore_errors' => false, - 'query_builder_method' => 'createQueryBuilder', - ), $options)); + public function __construct( + ObjectPersisterInterface $objectPersister, + IndexableInterface $indexable, + $objectClass, + array $baseOptions, + ManagerRegistry $managerRegistry, + SliceFetcherInterface $sliceFetcher = null + ) { + parent::__construct($objectPersister, $indexable, $objectClass, $baseOptions); $this->managerRegistry = $managerRegistry; - } - - /** - * @see FOS\ElasticaBundle\Provider\ProviderInterface::populate() - */ - public function populate(\Closure $loggerClosure = null, array $options = array()) - { - if (!$this->options['debug_logging']) { - $logger = $this->disableLogging(); - } - - $queryBuilder = $this->createQueryBuilder(); - $nbObjects = $this->countObjects($queryBuilder); - $offset = isset($options['offset']) ? intval($options['offset']) : 0; - $sleep = isset($options['sleep']) ? intval($options['sleep']) : 0; - $batchSize = isset($options['batch-size']) ? intval($options['batch-size']) : $this->options['batch_size']; - $ignoreErrors = isset($options['ignore-errors']) ? $options['ignore-errors'] : $this->options['ignore_errors']; - $manager = $this->managerRegistry->getManagerForClass($this->objectClass); - - for (; $offset < $nbObjects; $offset += $batchSize) { - if ($loggerClosure) { - $stepStartTime = microtime(true); - } - $objects = $this->fetchSlice($queryBuilder, $batchSize, $offset); - - if (!$ignoreErrors) { - $this->objectPersister->insertMany($objects); - } else { - try { - $this->objectPersister->insertMany($objects); - } catch(BulkResponseException $e) { - if ($loggerClosure) { - $loggerClosure(sprintf('%s',$e->getMessage())); - } - } - } - - if ($this->options['clear_object_manager']) { - $manager->clear(); - } - - usleep($sleep); - - if ($loggerClosure) { - $stepNbObjects = count($objects); - $stepCount = $stepNbObjects + $offset; - $percentComplete = 100 * $stepCount / $nbObjects; - $timeDifference = microtime(true) - $stepStartTime; - $objectsPerSecond = $timeDifference ? ($stepNbObjects / $timeDifference) : $stepNbObjects; - $loggerClosure(sprintf('%0.1f%% (%d/%d), %d objects/s %s', $percentComplete, $stepCount, $nbObjects, $objectsPerSecond, $this->getMemoryUsage())); - } - } - - if (!$this->options['debug_logging']) { - $this->enableLogging($logger); - } + $this->sliceFetcher = $sliceFetcher; } /** * Counts objects that would be indexed using the query builder. * * @param object $queryBuilder + * * @return integer */ - protected abstract function countObjects($queryBuilder); + abstract protected function countObjects($queryBuilder); /** - * Disables logging and returns the logger that was previously set. + * Creates the query builder, which will be used to fetch objects to index. * - * @return mixed - */ - protected abstract function disableLogging(); - - /** - * Reenables the logger with the previously returned logger from disableLogging(); + * @param string $method * - * @param mixed $logger - * @return mixed + * @return object */ - protected abstract function enableLogging($logger); + abstract protected function createQueryBuilder($method); /** * Fetches a slice of objects using the query builder. @@ -116,14 +69,102 @@ abstract class AbstractProvider extends BaseAbstractProvider * @param object $queryBuilder * @param integer $limit * @param integer $offset + * * @return array */ - protected abstract function fetchSlice($queryBuilder, $limit, $offset); + abstract protected function fetchSlice($queryBuilder, $limit, $offset); /** - * Creates the query builder, which will be used to fetch objects to index. - * - * @return object + * {@inheritDoc} */ - protected abstract function createQueryBuilder(); + protected function doPopulate($options, \Closure $loggerClosure = null) + { + $manager = $this->managerRegistry->getManagerForClass($this->objectClass); + + $queryBuilder = $this->createQueryBuilder($options['query_builder_method']); + $nbObjects = $this->countObjects($queryBuilder); + $offset = $options['offset']; + + $objects = array(); + for (; $offset < $nbObjects; $offset += $options['batch_size']) { + try { + $objects = $this->getSlice($queryBuilder, $options['batch_size'], $offset, $objects); + $objects = $this->filterObjects($options, $objects); + + if (!empty($objects)) { + $this->objectPersister->insertMany($objects); + } + } catch (BulkResponseException $e) { + if (!$options['ignore_errors']) { + throw $e; + } + + if (null !== $loggerClosure) { + $loggerClosure( + $options['batch_size'], + $nbObjects, + sprintf('%s', $e->getMessage()) + ); + } + } + + if ($options['clear_object_manager']) { + $manager->clear(); + } + + usleep($options['sleep']); + + if (null !== $loggerClosure) { + $loggerClosure($options['batch_size'], $nbObjects); + } + } + } + + /** + * {@inheritDoc} + */ + protected function configureOptions() + { + parent::configureOptions(); + + $this->resolver->setDefaults(array( + 'clear_object_manager' => true, + 'debug_logging' => false, + 'ignore_errors' => false, + 'offset' => 0, + 'query_builder_method' => 'createQueryBuilder', + 'sleep' => 0 + )); + } + + /** + * If this Provider has a SliceFetcher defined, we use it instead of falling back to + * the fetchSlice methods defined in the ORM/MongoDB subclasses. + * + * @param $queryBuilder + * @param int $limit + * @param int $offset + * @param array $lastSlice + * + * @return array + */ + private function getSlice($queryBuilder, $limit, $offset, $lastSlice) + { + if (!$this->sliceFetcher) { + return $this->fetchSlice($queryBuilder, $limit, $offset); + } + + $manager = $this->managerRegistry->getManagerForClass($this->objectClass); + $identifierFieldNames = $manager + ->getClassMetadata($this->objectClass) + ->getIdentifierFieldNames(); + + return $this->sliceFetcher->fetch( + $queryBuilder, + $limit, + $offset, + $lastSlice, + $identifierFieldNames + ); + } } diff --git a/Doctrine/Listener.php b/Doctrine/Listener.php index ff9fc60..a1d3585 100644 --- a/Doctrine/Listener.php +++ b/Doctrine/Listener.php @@ -2,238 +2,117 @@ namespace FOS\ElasticaBundle\Doctrine; -use Psr\Log\LoggerInterface; -use Doctrine\Common\EventArgs; -use Doctrine\Common\EventSubscriber; -use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; +use Doctrine\Common\Persistence\Event\LifecycleEventArgs; use FOS\ElasticaBundle\Persister\ObjectPersister; -use Symfony\Component\ExpressionLanguage\Expression; -use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -use Symfony\Component\ExpressionLanguage\SyntaxError; +use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; +use FOS\ElasticaBundle\Provider\IndexableInterface; +use Psr\Log\LoggerInterface; 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 +class Listener { /** - * Object persister + * Object persister. * - * @var ObjectPersister + * @var ObjectPersisterInterface */ protected $objectPersister; /** - * Class of the domain model - * - * @var string - */ - protected $objectClass; - - /** - * List of subscribed events + * Configuration for the listener. * * @var array */ - protected $events; + private $config; /** - * Name of domain model field used as the ES identifier + * Objects scheduled for insertion. * - * @var string - */ - protected $esIdentifierField; - - /** - * Callback for determining if an object should be indexed - * - * @var mixed - */ - protected $isIndexableCallback; - - /** - * Objects scheduled for insertion and replacement + * @var array */ public $scheduledForInsertion = array(); + + /** + * Objects scheduled to be updated or removed. + * + * @var array + */ public $scheduledForUpdate = array(); /** - * IDs of objects scheduled for removal + * IDs of objects scheduled for removal. + * + * @var array */ public $scheduledForDeletion = array(); /** - * An instance of ExpressionLanguage - * - * @var ExpressionLanguage - */ - protected $expressionLanguage; - - /** - * PropertyAccessor instance + * PropertyAccessor instance. * * @var PropertyAccessorInterface */ protected $propertyAccessor; + /** + * @var IndexableInterface + */ + private $indexable; + /** * Constructor. * * @param ObjectPersisterInterface $objectPersister - * @param string $objectClass - * @param array $events - * @param string $esIdentifierField + * @param IndexableInterface $indexable + * @param array $config + * @param LoggerInterface $logger */ - public function __construct(ObjectPersisterInterface $objectPersister, $objectClass, array $events, $esIdentifierField = 'id', $logger = null) - { - $this->objectPersister = $objectPersister; - $this->objectClass = $objectClass; - $this->events = $events; - $this->esIdentifierField = $esIdentifierField; + public function __construct( + ObjectPersisterInterface $objectPersister, + IndexableInterface $indexable, + array $config = array(), + LoggerInterface $logger = null + ) { + $this->config = array_merge(array( + 'identifier' => 'id', + ), $config); + $this->indexable = $indexable; + $this->objectPersister = $objectPersister; + $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); - if ($logger) { + if ($logger && $this->objectPersister instanceof ObjectPersister) { $this->objectPersister->setLogger($logger); } - - $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); } /** - * @see Doctrine\Common\EventSubscriber::getSubscribedEvents() - */ - public function getSubscribedEvents() - { - return $this->events; - } - - /** - * 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()); - } - - /** - * Provides unified method for retrieving a doctrine object from an EventArgs instance - * - * @param EventArgs $eventArgs - * @return object Entity | Document - * @throws \RuntimeException if no valid getter is found. - */ - private function getDoctrineObject(EventArgs $eventArgs) - { - if (method_exists($eventArgs, 'getObject')) { - return $eventArgs->getObject(); - } elseif (method_exists($eventArgs, 'getEntity')) { - return $eventArgs->getEntity(); - } elseif (method_exists($eventArgs, 'getDocument')) { - return $eventArgs->getDocument(); - } - - throw new \RuntimeException('Unable to retrieve object from EventArgs.'); - } - - /** - * @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 = $this->getDoctrineObject($eventArgs); - - 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 = $this->getDoctrineObject($eventArgs); + $entity = $eventArgs->getObject(); - if ($entity instanceof $this->objectClass) { + if ($this->objectPersister->handlesObject($entity)) { if ($this->isObjectIndexable($entity)) { $this->scheduledForUpdate[] = $entity; } else { @@ -245,20 +124,22 @@ class Listener implements EventSubscriber /** * Delete objects preRemove instead of postRemove so that we have access to the id. Because this is called - * preRemove, first check that the entity is managed by Doctrine + * preRemove, first check that the entity is managed by Doctrine. + * + * @param LifecycleEventArgs $eventArgs */ - public function preRemove(EventArgs $eventArgs) + public function preRemove(LifecycleEventArgs $eventArgs) { - $entity = $this->getDoctrineObject($eventArgs); + $entity = $eventArgs->getObject(); - if ($entity instanceof $this->objectClass) { + 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 + * After persisting, clear the scheduled queue to prevent multiple data updates when using multiple flush calls. */ private function persistScheduled() { @@ -277,32 +158,55 @@ class Listener implements EventSubscriber } /** - * Iterate through scheduled actions before flushing to emulate 2.x behavior. Note that the ElasticSearch index - * will fall out of sync with the source data in the event of a crash during flush. + * Iterate through scheduled actions before flushing to emulate 2.x behavior. + * Note that the ElasticSearch index will fall out of sync with the source + * data in the event of a crash during flush. + * + * This method is only called in legacy configurations of the listener. + * + * @deprecated This method should only be called in applications that depend + * on the behaviour that entities are indexed regardless of if a + * flush is successful. */ - public function preFlush(EventArgs $eventArgs) + public function preFlush() { $this->persistScheduled(); } /** - * Iterating through scheduled actions *after* flushing ensures that the ElasticSearch index will be affected - * only if the query is successful + * Iterating through scheduled actions *after* flushing ensures that the + * ElasticSearch index will be affected only if the query is successful. */ - public function postFlush(EventArgs $eventArgs) + public function postFlush() { $this->persistScheduled(); } /** * Record the specified identifier to delete. Do not need to entire object. - * @param mixed $object - * @return mixed + * + * @param object $object */ - protected function scheduleForDeletion($object) + private function scheduleForDeletion($object) { - if ($identifierValue = $this->propertyAccessor->getValue($object, $this->esIdentifierField)) { + 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..23a8292 100644 --- a/Doctrine/MongoDB/ElasticaToModelTransformer.php +++ b/Doctrine/MongoDB/ElasticaToModelTransformer.php @@ -7,21 +7,23 @@ use FOS\ElasticaBundle\Doctrine\AbstractElasticaToModelTransformer; /** * Maps Elastica documents with Doctrine objects * This mapper assumes an exact match between - * elastica documents ids and doctrine object ids + * elastica documents ids and doctrine object ids. */ class ElasticaToModelTransformer extends AbstractElasticaToModelTransformer { /** - * Fetch objects for theses identifier values + * Fetch objects for theses identifier values. + * + * @param array $identifierValues ids values + * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays * - * @param array $identifierValues ids values - * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays * @return array of objects or arrays */ protected function findByIdentifiers(array $identifierValues, $hydrate) { 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 9e1c5dd..e4b08c5 100644 --- a/Doctrine/MongoDB/Provider.php +++ b/Doctrine/MongoDB/Provider.php @@ -27,9 +27,10 @@ class Provider extends AbstractProvider } /** - * Reenables the logger with the previously returned logger from disableLogging(); + * Reenables the logger with the previously returned logger from disableLogging();. * * @param mixed $logger + * * @return mixed */ protected function enableLogging($logger) @@ -43,7 +44,7 @@ class Provider extends AbstractProvider } /** - * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::countObjects() + * {@inheritDoc} */ protected function countObjects($queryBuilder) { @@ -57,7 +58,7 @@ class Provider extends AbstractProvider } /** - * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::fetchSlice() + * {@inheritDoc} */ protected function fetchSlice($queryBuilder, $limit, $offset) { @@ -66,21 +67,21 @@ class Provider extends AbstractProvider } return $queryBuilder - ->limit($limit) ->skip($offset) + ->limit($limit) ->getQuery() ->execute() ->toArray(); } /** - * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::createQueryBuilder() + * {@inheritDoc} */ - protected function createQueryBuilder() + protected function createQueryBuilder($method) { return $this->managerRegistry ->getManagerForClass($this->objectClass) ->getRepository($this->objectClass) - ->{$this->options['query_builder_method']}(); + ->{$method}(); } } diff --git a/Doctrine/MongoDB/SliceFetcher.php b/Doctrine/MongoDB/SliceFetcher.php new file mode 100644 index 0000000..4723da6 --- /dev/null +++ b/Doctrine/MongoDB/SliceFetcher.php @@ -0,0 +1,44 @@ + + */ +class SliceFetcher implements SliceFetcherInterface +{ + /** + * {@inheritdoc} + */ + public function fetch($queryBuilder, $limit, $offset, array $previousSlice, array $identifierFieldNames) + { + if (!$queryBuilder instanceof Builder) { + throw new InvalidArgumentTypeException($queryBuilder, 'Doctrine\ODM\MongoDB\Query\Builder'); + } + + $lastObject = array_pop($previousSlice); + + if ($lastObject) { + $queryBuilder + ->field('_id')->gt($lastObject->getId()) + ->skip(0) + ; + } else { + $queryBuilder->skip($offset); + } + + return $queryBuilder + ->limit($limit) + ->sort(array('_id' => 'asc')) + ->getQuery() + ->execute() + ->toArray() + ; + } +} diff --git a/Doctrine/ORM/ElasticaToModelTransformer.php b/Doctrine/ORM/ElasticaToModelTransformer.php index a57a84c..21d8640 100644 --- a/Doctrine/ORM/ElasticaToModelTransformer.php +++ b/Doctrine/ORM/ElasticaToModelTransformer.php @@ -8,17 +8,18 @@ use Doctrine\ORM\Query; /** * Maps Elastica documents with Doctrine objects * This mapper assumes an exact match between - * elastica documents ids and doctrine object ids + * elastica documents ids and doctrine object ids. */ class ElasticaToModelTransformer extends AbstractElasticaToModelTransformer { const ENTITY_ALIAS = 'o'; /** - * Fetch objects for theses identifier values + * Fetch objects for theses identifier values. + * + * @param array $identifierValues ids values + * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays * - * @param array $identifierValues ids values - * @param Boolean $hydrate whether or not to hydrate the objects, false returns arrays * @return array of objects or arrays */ protected function findByIdentifiers(array $identifierValues, $hydrate) @@ -36,7 +37,7 @@ class ElasticaToModelTransformer extends AbstractElasticaToModelTransformer } /** - * Retrieves a query builder to be used for querying by identifiers + * Retrieves a query builder to be used for querying by identifiers. * * @return \Doctrine\ORM\QueryBuilder */ diff --git a/Doctrine/ORM/Provider.php b/Doctrine/ORM/Provider.php index dfd6700..85b5279 100644 --- a/Doctrine/ORM/Provider.php +++ b/Doctrine/ORM/Provider.php @@ -3,7 +3,6 @@ namespace FOS\ElasticaBundle\Doctrine\ORM; use Doctrine\ORM\QueryBuilder; -use Elastica\Exception\Bulk\ResponseException as BulkResponseException; use FOS\ElasticaBundle\Doctrine\AbstractProvider; use FOS\ElasticaBundle\Exception\InvalidArgumentTypeException; @@ -30,9 +29,10 @@ class Provider extends AbstractProvider } /** - * Reenables the logger with the previously returned logger from disableLogging(); + * Reenables the logger with the previously returned logger from disableLogging();. * * @param mixed $logger + * * @return mixed */ protected function enableLogging($logger) @@ -46,7 +46,7 @@ class Provider extends AbstractProvider } /** - * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::countObjects() + * {@inheritDoc} */ protected function countObjects($queryBuilder) { @@ -69,7 +69,9 @@ class Provider extends AbstractProvider } /** - * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::fetchSlice() + * This method should remain in sync with SliceFetcher::fetch until it is deprecated and removed. + * + * {@inheritDoc} */ protected function fetchSlice($queryBuilder, $limit, $offset) { @@ -77,8 +79,8 @@ class Provider extends AbstractProvider throw new InvalidArgumentTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder'); } - /** - * An orderBy DQL part is required to avoid feching the same row twice. + /* + * An orderBy DQL part is required to avoid fetching the same row twice. * @see http://stackoverflow.com/questions/6314879/does-limit-offset-length-require-order-by-for-pagination * @see http://www.postgresql.org/docs/current/static/queries-limit.html * @see http://www.sqlite.org/lang_select.html#orderby @@ -103,14 +105,14 @@ class Provider extends AbstractProvider } /** - * @see FOS\ElasticaBundle\Doctrine\AbstractProvider::createQueryBuilder() + * {@inheritDoc} */ - protected function createQueryBuilder() + protected function createQueryBuilder($method) { return $this->managerRegistry ->getManagerForClass($this->objectClass) ->getRepository($this->objectClass) // ORM query builders require an alias argument - ->{$this->options['query_builder_method']}(static::ENTITY_ALIAS); + ->{$method}(static::ENTITY_ALIAS); } } diff --git a/Doctrine/ORM/SliceFetcher.php b/Doctrine/ORM/SliceFetcher.php new file mode 100644 index 0000000..ac6c816 --- /dev/null +++ b/Doctrine/ORM/SliceFetcher.php @@ -0,0 +1,50 @@ + + */ +class SliceFetcher implements SliceFetcherInterface +{ + /** + * This method should remain in sync with Provider::fetchSlice until that method is deprecated and + * removed. + * + * {@inheritdoc} + */ + public function fetch($queryBuilder, $limit, $offset, array $previousSlice, array $identifierFieldNames) + { + if (!$queryBuilder instanceof QueryBuilder) { + throw new InvalidArgumentTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder'); + } + + /* + * An orderBy DQL part is required to avoid feching the same row twice. + * @see http://stackoverflow.com/questions/6314879/does-limit-offset-length-require-order-by-for-pagination + * @see http://www.postgresql.org/docs/current/static/queries-limit.html + * @see http://www.sqlite.org/lang_select.html#orderby + */ + $orderBy = $queryBuilder->getDQLPart('orderBy'); + if (empty($orderBy)) { + $rootAliases = $queryBuilder->getRootAliases(); + + foreach ($identifierFieldNames as $fieldName) { + $queryBuilder->addOrderBy($rootAliases[0].'.'.$fieldName); + } + } + + return $queryBuilder + ->setFirstResult($offset) + ->setMaxResults($limit) + ->getQuery() + ->getResult() + ; + } +} diff --git a/Doctrine/RepositoryManager.php b/Doctrine/RepositoryManager.php index f8867eb..0d20f64 100644 --- a/Doctrine/RepositoryManager.php +++ b/Doctrine/RepositoryManager.php @@ -25,7 +25,7 @@ class RepositoryManager extends BaseManager } /** - * Return repository for entity + * Return repository for entity. * * Returns custom repository if one specified otherwise * returns a basic repository. @@ -35,7 +35,7 @@ class RepositoryManager extends BaseManager $realEntityName = $entityName; if (strpos($entityName, ':') !== false) { list($namespaceAlias, $simpleClassName) = explode(':', $entityName); - $realEntityName = $this->managerRegistry->getAliasNamespace($namespaceAlias) . '\\' . $simpleClassName; + $realEntityName = $this->managerRegistry->getAliasNamespace($namespaceAlias).'\\'.$simpleClassName; } return parent::getRepository($realEntityName); diff --git a/Doctrine/SliceFetcherInterface.php b/Doctrine/SliceFetcherInterface.php new file mode 100644 index 0000000..a028abf --- /dev/null +++ b/Doctrine/SliceFetcherInterface.php @@ -0,0 +1,24 @@ + + */ +interface SliceFetcherInterface +{ + /** + * Fetches a slice of objects using the query builder. + * + * @param object $queryBuilder + * @param integer $limit + * @param integer $offset + * @param array $previousSlice + * @param array $identifierFieldNames + * + * @return array + */ + public function fetch($queryBuilder, $limit, $offset, array $previousSlice, array $identifierFieldNames); +} diff --git a/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..c244eb4 --- /dev/null +++ b/Elastica/Client.php @@ -0,0 +1,104 @@ + + */ +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..ad3bd4d --- /dev/null +++ b/Elastica/Index.php @@ -0,0 +1,59 @@ + + */ +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 + */ + public function overrideName($name) + { + $this->originalName = $this->_name; + $this->_name = $name; + } +} diff --git a/Event/IndexEvent.php b/Event/IndexEvent.php new file mode 100644 index 0000000..ed71d78 --- /dev/null +++ b/Event/IndexEvent.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Event; + +use Symfony\Component\EventDispatcher\Event; + +class IndexEvent extends Event +{ + /** + * @var string + */ + private $index; + + /** + * @param string $index + */ + public function __construct($index) + { + $this->index = $index; + } + + /** + * @return string + */ + public function getIndex() + { + return $this->index; + } +} diff --git a/Event/IndexPopulateEvent.php b/Event/IndexPopulateEvent.php new file mode 100644 index 0000000..b35105a --- /dev/null +++ b/Event/IndexPopulateEvent.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Event; + +/** + * Index Populate Event. + * + * @author Oleg Andreyev + */ +class IndexPopulateEvent extends IndexEvent +{ + const PRE_INDEX_POPULATE = 'elastica.index.index_pre_populate'; + const POST_INDEX_POPULATE = 'elastica.index.index_post_populate'; + + /** + * @var bool + */ + private $reset; + + /** + * @var array + */ + private $options; + + /** + * @param string $index + * @param boolean $reset + * @param array $options + */ + public function __construct($index, $reset, $options) + { + parent::__construct($index); + + $this->reset = $reset; + $this->options = $options; + } + + /** + * @return boolean + */ + public function isReset() + { + return $this->reset; + } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * @param boolean $reset + */ + public function setReset($reset) + { + $this->reset = $reset; + } +} diff --git a/Event/IndexResetEvent.php b/Event/IndexResetEvent.php new file mode 100644 index 0000000..871915a --- /dev/null +++ b/Event/IndexResetEvent.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Event; + +/** + * Index ResetEvent. + * + * @author Oleg Andreyev + */ +class IndexResetEvent extends IndexEvent +{ + const PRE_INDEX_RESET = 'elastica.index.pre_reset'; + const POST_INDEX_RESET = 'elastica.index.post_reset'; + + /** + * @var bool + */ + private $force; + + /** + * @var bool + */ + private $populating; + + /** + * @param string $index + * @param bool $populating + * @param bool $force + */ + public function __construct($index, $populating, $force) + { + parent::__construct($index); + + $this->force = $force; + $this->populating = $populating; + } + + /** + * @return boolean + */ + public function isForce() + { + return $this->force; + } + + /** + * @return boolean + */ + public function isPopulating() + { + return $this->populating; + } + + /** + * @param boolean $force + */ + public function setForce($force) + { + $this->force = $force; + } +} diff --git a/Event/TransformEvent.php b/Event/TransformEvent.php new file mode 100644 index 0000000..4f6871f --- /dev/null +++ b/Event/TransformEvent.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Event; + +use Symfony\Component\EventDispatcher\Event; + +class TransformEvent extends Event +{ + const POST_TRANSFORM = 'fos_elastica.post_transform'; + + /** + * @var mixed + */ + private $document; + + /** + * @var array + */ + private $fields; + + /** + * @var mixed + */ + private $object; + + /** + * @param mixed $document + * @param array $fields + * @param mixed $object + */ + public function __construct($document, array $fields, $object) + { + $this->document = $document; + $this->fields = $fields; + $this->object = $object; + } + + /** + * @return mixed + */ + public function getDocument() + { + return $this->document; + } + + /** + * @return array + */ + public function getFields() + { + return $this->fields; + } + + /** + * @return mixed + */ + public function getObject() + { + return $this->object; + } + + /** + * @param mixed $document + */ + public function setDocument($document) + { + $this->document = $document; + } +} diff --git a/Event/TypePopulateEvent.php b/Event/TypePopulateEvent.php new file mode 100644 index 0000000..dd744f5 --- /dev/null +++ b/Event/TypePopulateEvent.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Event; + +use Symfony\Component\EventDispatcher\Event; + +/** + * Type Populate Event. + * + * @author Oleg Andreyev + */ +class TypePopulateEvent extends IndexPopulateEvent +{ + const PRE_TYPE_POPULATE = 'elastica.index.type_pre_populate'; + const POST_TYPE_POPULATE = 'elastica.index.type_post_populate'; + + /** + * @var string + */ + private $type; + + /** + * @param string $index + * @param string $type + * @param bool $reset + * @param array $options + */ + public function __construct($index, $type, $reset, $options) + { + parent::__construct($index, $reset, $options); + + $this->type = $type; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } +} diff --git a/Event/TypeResetEvent.php b/Event/TypeResetEvent.php new file mode 100644 index 0000000..98fa2f4 --- /dev/null +++ b/Event/TypeResetEvent.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Event; + +use Symfony\Component\EventDispatcher\Event; + +/** + * Type ResetEvent. + * + * @author Oleg Andreyev + */ +class TypeResetEvent extends IndexEvent +{ + const PRE_TYPE_RESET = 'elastica.index.type_pre_reset'; + const POST_TYPE_RESET = 'elastica.index.type_post_reset'; + + /** + * @var string + */ + private $type; + + /** + * @param string $index + * @param string $type + */ + public function __construct($index, $type) + { + parent::__construct($index); + + $this->type = $type; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } +} diff --git a/Exception/AliasIsIndexException.php b/Exception/AliasIsIndexException.php new file mode 100644 index 0000000..9af6ee3 --- /dev/null +++ b/Exception/AliasIsIndexException.php @@ -0,0 +1,11 @@ +addCompilerPass(new ConfigSourcePass()); + $container->addCompilerPass(new IndexPass()); $container->addCompilerPass(new RegisterProvidersPass(), PassConfig::TYPE_BEFORE_REMOVING); $container->addCompilerPass(new TransformerPass()); } diff --git a/Finder/FinderInterface.php b/Finder/FinderInterface.php index 7c257de..86dbf86 100644 --- a/Finder/FinderInterface.php +++ b/Finder/FinderInterface.php @@ -5,12 +5,13 @@ namespace FOS\ElasticaBundle\Finder; interface FinderInterface { /** - * Searches for query results within a given limit + * Searches for query results within a given limit. * - * @param mixed $query Can be a string, an array or an \Elastica\Query object - * @param int $limit How many results to get + * @param mixed $query Can be a string, an array or an \Elastica\Query object + * @param int $limit How many results to get * @param array $options + * * @return array results */ - function find($query, $limit = null, $options = array()); + public function find($query, $limit = null, $options = array()); } diff --git a/Finder/PaginatedFinderInterface.php b/Finder/PaginatedFinderInterface.php index fa10b70..1fc7a48 100644 --- a/Finder/PaginatedFinderInterface.php +++ b/Finder/PaginatedFinderInterface.php @@ -9,20 +9,22 @@ use Elastica\Query; interface PaginatedFinderInterface extends FinderInterface { /** - * Searches for query results and returns them wrapped in a paginator + * Searches for query results and returns them wrapped in a paginator. * - * @param mixed $query Can be a string, an array or an \Elastica\Query object + * @param mixed $query Can be a string, an array or an \Elastica\Query object * @param array $options + * * @return Pagerfanta paginated results */ - function findPaginated($query, $options = array()); + public function findPaginated($query, $options = array()); /** - * Creates a paginator adapter for this query + * Creates a paginator adapter for this query. * * @param mixed $query * @param array $options + * * @return PaginatorAdapterInterface */ - function createPaginatorAdapter($query, $options = array()); + public function createPaginatorAdapter($query, $options = array()); } diff --git a/Finder/TransformedFinder.php b/Finder/TransformedFinder.php index 9080701..44f6d2f 100644 --- a/Finder/TransformedFinder.php +++ b/Finder/TransformedFinder.php @@ -11,7 +11,7 @@ use Elastica\SearchableInterface; use Elastica\Query; /** - * Finds elastica documents and map them to persisted objects + * Finds elastica documents and map them to persisted objects. */ class TransformedFinder implements PaginatedFinderInterface { @@ -25,11 +25,12 @@ class TransformedFinder implements PaginatedFinderInterface } /** - * Search for a query string + * Search for a query string. * - * @param string $query + * @param string $query * @param integer $limit - * @param array $options + * @param array $options + * * @return array of model objects **/ public function find($query, $limit = null, $options = array()) @@ -50,8 +51,9 @@ class TransformedFinder implements PaginatedFinderInterface * Find documents similar to one with passed id. * * @param integer $id - * @param array $params - * @param array $query + * @param array $params + * @param array $query + * * @return array of model objects **/ public function moreLikeThis($id, $params = array(), $query = array()) @@ -65,7 +67,8 @@ class TransformedFinder implements PaginatedFinderInterface /** * @param $query * @param null|int $limit - * @param array $options + * @param array $options + * * @return array */ protected function search($query, $limit = null, $options = array()) @@ -80,10 +83,11 @@ class TransformedFinder implements PaginatedFinderInterface } /** - * Gets a paginator wrapping the result of a search + * Gets a paginator wrapping the result of a search. * * @param string $query - * @param array $options + * @param array $options + * * @return Pagerfanta */ public function findPaginated($query, $options = array()) diff --git a/HybridResult.php b/HybridResult.php index ebd0e99..81499ba 100644 --- a/HybridResult.php +++ b/HybridResult.php @@ -24,4 +24,4 @@ class HybridResult { return $this->result; } -} \ No newline at end of file +} diff --git a/Index/AliasProcessor.php b/Index/AliasProcessor.php new file mode 100644 index 0000000..d8f608d --- /dev/null +++ b/Index/AliasProcessor.php @@ -0,0 +1,197 @@ + + * + * 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 = null; + $newIndexName = $index->getName(); + + try { + $oldIndexName = $this->getAliasedIndex($client, $aliasName); + } catch (AliasIsIndexException $e) { + if (!$force) { + throw $e; + } + + $this->deleteIndex($client, $aliasName); + } + + try { + $aliasUpdateRequest = $this->buildAliasUpdateRequest($oldIndexName, $aliasName, $newIndexName); + $client->request('_aliases', 'POST', $aliasUpdateRequest); + } catch (ExceptionInterface $e) { + $this->cleanupRenameFailure($client, $newIndexName, $e); + } + + // Delete the old index after the alias has been switched + if (null !== $oldIndexName) { + $this->deleteIndex($client, $oldIndexName); + } + } + + /** + * Builds an ElasticSearch request to rename or create an alias. + * + * @param string|null $aliasedIndex + * @param string $aliasName + * @param string $newIndexName + * @return array + */ + private function buildAliasUpdateRequest($aliasedIndex, $aliasName, $newIndexName) + { + $aliasUpdateRequest = array('actions' => array()); + if (null !== $aliasedIndex) { + // if the alias is set - add an action to remove it + $aliasUpdateRequest['actions'][] = array( + 'remove' => array('index' => $aliasedIndex, 'alias' => $aliasName), + ); + } + + // add an action to point the alias to the new index + $aliasUpdateRequest['actions'][] = array( + 'add' => array('index' => $newIndexName, 'alias' => $aliasName), + ); + + return $aliasUpdateRequest; + } + + /** + * Cleans up an index when we encounter a failure to rename the alias. + * + * @param Client $client + * @param string $indexName + * @param \Exception $renameAliasException + */ + private function cleanupRenameFailure(Client $client, $indexName, \Exception $renameAliasException) + { + $additionalError = ''; + try { + $this->deleteIndex($client, $indexName); + } catch (ExceptionInterface $deleteNewIndexException) { + $additionalError = sprintf( + 'Tried to delete newly built index %s, but also failed: %s', + $indexName, + $deleteNewIndexException->getMessage() + ); + } + + throw new \RuntimeException(sprintf( + 'Failed to updated index alias: %s. %s', + $renameAliasException->getMessage(), + $additionalError ?: sprintf('Newly built index %s was deleted', $indexName) + ), 0, $renameAliasException); + } + + /** + * Delete an index. + * + * @param Client $client + * @param string $indexName Index name to delete + */ + private function deleteIndex(Client $client, $indexName) + { + try { + $path = sprintf("%s", $indexName); + $client->request($path, Request::DELETE); + } catch (ExceptionInterface $deleteOldIndexException) { + throw new \RuntimeException(sprintf( + 'Failed to delete index %s with message: %s', + $indexName, + $deleteOldIndexException->getMessage() + ), 0, $deleteOldIndexException); + } + } + + /** + * Returns the name of a single index that an alias points to or throws + * an exception if there is more than one. + * + * @param Client $client + * @param string $aliasName Alias name + * + * @return string|null + * + * @throws AliasIsIndexException + */ + private function getAliasedIndex(Client $client, $aliasName) + { + $aliasesInfo = $client->request('_aliases', 'GET')->getData(); + $aliasedIndexes = array(); + + foreach ($aliasesInfo as $indexName => $indexInfo) { + if ($indexName === $aliasName) { + throw new AliasIsIndexException($indexName); + } + if (!isset($indexInfo['aliases'])) { + continue; + } + + $aliases = array_keys($indexInfo['aliases']); + if (in_array($aliasName, $aliases)) { + $aliasedIndexes[] = $indexName; + } + } + + if (count($aliasedIndexes) > 1) { + throw new \RuntimeException(sprintf( + 'Alias %s is used for multiple indexes: [%s]. Make sure it\'s'. + 'either not used or is assigned to one index only', + $aliasName, + implode(', ', $aliasedIndexes) + )); + } + + return array_shift($aliasedIndexes); + } +} diff --git a/Index/IndexManager.php b/Index/IndexManager.php new file mode 100644 index 0000000..98ce870 --- /dev/null +++ b/Index/IndexManager.php @@ -0,0 +1,70 @@ +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..e03bf54 --- /dev/null +++ b/Index/MappingBuilder.php @@ -0,0 +1,134 @@ + + * + * 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 (!empty($typeMappings)) { + $mapping['mappings'] = $typeMappings; + } + // 'warmers' => $indexConfig->getWarmers(), + + $settings = $indexConfig->getSettings(); + if (!empty($settings)) { + $mapping['settings'] = $settings; + } + + return $mapping; + } + + /** + * Builds mappings for a single type. + * + * @param TypeConfig $typeConfig + * + * @return array + */ + public function buildTypeMapping(TypeConfig $typeConfig) + { + $mapping = $typeConfig->getMapping(); + + if (null !== $typeConfig->getDynamicDateFormats()) { + $mapping['dynamic_date_formats'] = $typeConfig->getDynamicDateFormats(); + } + + if (null !== $typeConfig->getDateDetection()) { + $mapping['date_detection'] = $typeConfig->getDateDetection(); + } + + if (null !== $typeConfig->getNumericDetection()) { + $mapping['numeric_detection'] = $typeConfig->getNumericDetection(); + } + + if ($typeConfig->getIndexAnalyzer()) { + $mapping['index_analyzer'] = $typeConfig->getIndexAnalyzer(); + } + + 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 (empty($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) { + unset($property['property_path']); + + if (!isset($property['type'])) { + $property['type'] = 'string'; + } + if ($property['type'] == 'multi_field' && isset($property['fields'])) { + $this->fixProperties($property['fields']); + } + if (isset($property['properties'])) { + $this->fixProperties($property['properties']); + } + 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..2b39157 --- /dev/null +++ b/Index/Resetter.php @@ -0,0 +1,158 @@ +aliasProcessor = $aliasProcessor; + $this->configManager = $configManager; + $this->dispatcher = $eventDispatcher; + $this->indexManager = $indexManager; + $this->mappingBuilder = $mappingBuilder; + } + + /** + * Deletes and recreates all indexes. + * + * @param bool $populating + * @param bool $force + */ + 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); + + $event = new IndexResetEvent($indexName, $populating, $force); + $this->dispatcher->dispatch(IndexResetEvent::PRE_INDEX_RESET, $event); + + 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); + } + + $this->dispatcher->dispatch(IndexResetEvent::POST_INDEX_RESET, $event); + } + + /** + * 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); + + $event = new TypeResetEvent($indexName, $typeName); + $this->dispatcher->dispatch(TypeResetEvent::PRE_TYPE_RESET, $event); + + 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); + + $this->dispatcher->dispatch(TypeResetEvent::POST_TYPE_RESET, $event); + } + + /** + * 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..1a0601c 100644 --- a/Manager/RepositoryManager.php +++ b/Manager/RepositoryManager.php @@ -25,13 +25,13 @@ class RepositoryManager implements RepositoryManagerInterface public function addEntity($entityName, FinderInterface $finder, $repositoryName = null) { - $this->entities[$entityName]= array(); + $this->entities[$entityName] = array(); $this->entities[$entityName]['finder'] = $finder; $this->entities[$entityName]['repositoryName'] = $repositoryName; } /** - * Return repository for entity + * Return repository for entity. * * Returns custom repository if one specified otherwise * returns a basic repository. @@ -59,16 +59,20 @@ 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; + return $annotation->repositoryClass; } return 'FOS\ElasticaBundle\Repository'; } + /** + * @param string $entityName + */ private function createRepository($entityName) { if (!class_exists($repositoryName = $this->getRepositoryName($entityName))) { diff --git a/Manager/RepositoryManagerInterface.php b/Manager/RepositoryManagerInterface.php index 1008371..0a38e0e 100644 --- a/Manager/RepositoryManagerInterface.php +++ b/Manager/RepositoryManagerInterface.php @@ -12,7 +12,6 @@ use FOS\ElasticaBundle\Finder\FinderInterface; */ interface RepositoryManagerInterface { - /** * Adds entity name and its finder. * Custom repository class name can also be added. @@ -24,7 +23,7 @@ interface RepositoryManagerInterface public function addEntity($entityName, FinderInterface $finder, $repositoryName = null); /** - * Return repository for entity + * Return repository for entity. * * Returns custom repository if one specified otherwise * returns a basic repository. diff --git a/Paginator/FantaPaginatorAdapter.php b/Paginator/FantaPaginatorAdapter.php index 2ad6983..8f9a60a 100644 --- a/Paginator/FantaPaginatorAdapter.php +++ b/Paginator/FantaPaginatorAdapter.php @@ -20,8 +20,6 @@ class FantaPaginatorAdapter implements AdapterInterface * Returns the number of results. * * @return integer The number of results. - * - * @api */ public function getNbResults() { @@ -29,15 +27,25 @@ class FantaPaginatorAdapter implements AdapterInterface } /** - * Returns Facets + * Returns Facets. + * + * @return mixed + */ + public function getFacets() + { + return $this->adapter->getFacets(); + } + + /** + * Returns Aggregations. * * @return mixed * * @api */ - public function getFacets() + public function getAggregations() { - return $this->adapter->getFacets(); + return $this->adapter->getAggregations(); } /** @@ -47,8 +55,6 @@ class FantaPaginatorAdapter implements AdapterInterface * @param integer $length The length. * * @return array|\Traversable The slice. - * - * @api */ public function getSlice($offset, $length) { diff --git a/Paginator/PaginatorAdapterInterface.php b/Paginator/PaginatorAdapterInterface.php index 25786a0..adf7df2 100644 --- a/Paginator/PaginatorAdapterInterface.php +++ b/Paginator/PaginatorAdapterInterface.php @@ -8,10 +8,8 @@ interface PaginatorAdapterInterface * Returns the number of results. * * @return integer The number of results. - * - * @api */ - function getTotalHits(); + public function getTotalHits(); /** * Returns an slice of the results. @@ -20,15 +18,20 @@ interface PaginatorAdapterInterface * @param integer $length The length. * * @return PartialResultsInterface - * - * @api */ - function getResults($offset, $length); + public function getResults($offset, $length); /** - * Returns Facets + * Returns Facets. * * @return mixed */ - function getFacets(); + public function getFacets(); + + /** + * Returns Aggregations. + * + * @return mixed + */ + public function getAggregations(); } diff --git a/Paginator/PartialResultsInterface.php b/Paginator/PartialResultsInterface.php index 9efe7f3..156d27f 100644 --- a/Paginator/PartialResultsInterface.php +++ b/Paginator/PartialResultsInterface.php @@ -8,24 +8,27 @@ interface PartialResultsInterface * Returns the paginated results. * * @return array - * - * @api */ - function toArray(); + public function toArray(); /** * Returns the number of results. * * @return integer The number of results. - * - * @api */ - function getTotalHits(); + public function getTotalHits(); /** - * Returns the facets + * Returns the facets. * * @return array */ - function getFacets(); -} \ No newline at end of file + public function getFacets(); + + /** + * Returns the aggregations. + * + * @return array + */ + public function getAggregations(); +} diff --git a/Paginator/RawPaginatorAdapter.php b/Paginator/RawPaginatorAdapter.php index 9136bc0..2eebde0 100644 --- a/Paginator/RawPaginatorAdapter.php +++ b/Paginator/RawPaginatorAdapter.php @@ -8,7 +8,7 @@ use Elastica\ResultSet; use InvalidArgumentException; /** - * Allows pagination of Elastica\Query. Does not map results + * Allows pagination of Elastica\Query. Does not map results. */ class RawPaginatorAdapter implements PaginatorAdapterInterface { @@ -37,11 +37,16 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface */ private $facets; + /** + * @var array for the aggregations + */ + private $aggregations; + /** * @see PaginatorAdapterInterface::__construct * * @param SearchableInterface $searchable the object to search in - * @param Query $query the query to search + * @param Query $query the query to search * @param array $options */ public function __construct(SearchableInterface $searchable, Query $query, array $options = array()) @@ -54,9 +59,11 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface /** * Returns the paginated results. * - * @param $offset - * @param $itemCountPerPage + * @param integer $offset + * @param integer $itemCountPerPage + * * @throws \InvalidArgumentException + * * @return ResultSet */ protected function getElasticaResults($offset, $itemCountPerPage) @@ -67,7 +74,7 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface ? (integer) $this->query->getParam('size') : null; - if ($size && $size < $offset + $itemCountPerPage) { + if (null !== $size && $size < $offset + $itemCountPerPage) { $itemCountPerPage = $size - $offset; } @@ -82,6 +89,8 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface $resultSet = $this->searchable->search($query, $this->options); $this->totalHits = $resultSet->getTotalHits(); $this->facets = $resultSet->getFacets(); + $this->aggregations = $resultSet->getAggregations(); + return $resultSet; } @@ -90,6 +99,7 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface * * @param int $offset * @param int $itemCountPerPage + * * @return PartialResultsInterface */ public function getResults($offset, $itemCountPerPage) @@ -100,27 +110,33 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface /** * Returns the number of results. * + * If genuineTotal is provided as true, total hits is returned from the + * hits.total value from the search results instead of just returning + * the requested size. + * + * @param boolean $genuineTotal + * * @return integer The number of results. */ - public function getTotalHits() + public function getTotalHits($genuineTotal = false) { - if ( ! isset($this->totalHits)) { - $this->totalHits = $this->searchable->search($this->query)->getTotalHits(); + if (! isset($this->totalHits)) { + $this->totalHits = $this->searchable->count($this->query); } - return $this->query->hasParam('size') + return $this->query->hasParam('size') && !$genuineTotal ? min($this->totalHits, (integer) $this->query->getParam('size')) : $this->totalHits; } /** - * Returns Facets + * Returns Facets. * * @return mixed */ public function getFacets() { - if ( ! isset($this->facets)) { + if (! isset($this->facets)) { $this->facets = $this->searchable->search($this->query)->getFacets(); } @@ -128,7 +144,21 @@ class RawPaginatorAdapter implements PaginatorAdapterInterface } /** - * Returns the Query + * Returns Aggregations. + * + * @return mixed + */ + public function getAggregations() + { + if (!isset($this->aggregations)) { + $this->aggregations = $this->searchable->search($this->query)->getAggregations(); + } + + return $this->aggregations; + } + + /** + * Returns the Query. * * @return Query the search query */ diff --git a/Paginator/RawPartialResults.php b/Paginator/RawPartialResults.php index a4afb00..e45c6dd 100644 --- a/Paginator/RawPartialResults.php +++ b/Paginator/RawPartialResults.php @@ -6,7 +6,7 @@ use Elastica\ResultSet; use Elastica\Result; /** - * Raw partial results transforms to a simple array + * Raw partial results transforms to a simple array. */ class RawPartialResults implements PartialResultsInterface { @@ -25,7 +25,7 @@ class RawPartialResults implements PartialResultsInterface */ public function toArray() { - return array_map(function(Result $result) { + return array_map(function (Result $result) { return $result->getSource(); }, $this->resultSet->getResults()); } @@ -47,6 +47,18 @@ class RawPartialResults implements PartialResultsInterface return $this->resultSet->getFacets(); } - return null; + return; } -} \ No newline at end of file + + /** + * {@inheritDoc} + */ + public function getAggregations() + { + if ($this->resultSet->hasAggregations()) { + return $this->resultSet->getAggregations(); + } + + return; + } +} diff --git a/Paginator/TransformedPaginatorAdapter.php b/Paginator/TransformedPaginatorAdapter.php index 3b4716f..bf152fb 100644 --- a/Paginator/TransformedPaginatorAdapter.php +++ b/Paginator/TransformedPaginatorAdapter.php @@ -7,15 +7,15 @@ use Elastica\SearchableInterface; use Elastica\Query; /** - * Allows pagination of \Elastica\Query + * Allows pagination of \Elastica\Query. */ class TransformedPaginatorAdapter extends RawPaginatorAdapter { private $transformer; /** - * @param SearchableInterface $searchable the object to search in - * @param Query $query the query to search + * @param SearchableInterface $searchable the object to search in + * @param Query $query the query to search * @param array $options * @param ElasticaToModelTransformerInterface $transformer the transformer for fetching the results */ diff --git a/Paginator/TransformedPartialResults.php b/Paginator/TransformedPartialResults.php index 13d716c..c9470c3 100644 --- a/Paginator/TransformedPartialResults.php +++ b/Paginator/TransformedPartialResults.php @@ -6,14 +6,14 @@ use FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerInterface; use Elastica\ResultSet; /** - * Partial transformed result set + * Partial transformed result set. */ class TransformedPartialResults extends RawPartialResults { protected $transformer; /** - * @param ResultSet $resultSet + * @param ResultSet $resultSet * @param \FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerInterface $transformer */ public function __construct(ResultSet $resultSet, ElasticaToModelTransformerInterface $transformer) @@ -30,4 +30,4 @@ class TransformedPartialResults extends RawPartialResults { return $this->transformer->transform($this->resultSet->getResults()); } -} \ No newline at end of file +} diff --git a/Persister/ObjectPersister.php b/Persister/ObjectPersister.php index c279ec7..92dc005 100644 --- a/Persister/ObjectPersister.php +++ b/Persister/ObjectPersister.php @@ -4,14 +4,13 @@ namespace FOS\ElasticaBundle\Persister; use Psr\Log\LoggerInterface; use Elastica\Exception\BulkException; -use Elastica\Exception\NotFoundException; use FOS\ElasticaBundle\Transformer\ModelToElasticaTransformerInterface; use Elastica\Type; use Elastica\Document; /** * Inserts, replaces and deletes single documents in an elastica type - * Accepts domain model objects and converts them to elastica documents + * Accepts domain model objects and converts them to elastica documents. * * @author Thibault Duplessis */ @@ -31,17 +30,32 @@ 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; + } + + /** + * @param LoggerInterface $logger + */ public function setLogger(LoggerInterface $logger) { $this->logger = $logger; } /** - * Log exception if logger defined for persister belonging to the current listener, otherwise re-throw + * Log exception if logger defined for persister belonging to the current listener, otherwise re-throw. * * @param BulkException $e + * * @throws BulkException - * @return null */ private function log(BulkException $e) { @@ -54,61 +68,47 @@ class ObjectPersister implements ObjectPersisterInterface /** * Insert one object into the type - * The object will be transformed to an elastica document + * The object will be transformed to an elastica document. * * @param object $object */ public function insertOne($object) { - $document = $this->transformToElasticaDocument($object); - $this->type->addDocument($document); + $this->insertMany(array($object)); } /** - * Replaces one object in the type + * Replaces one object in the type. * * @param object $object - * @return null **/ 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)); } /** - * Deletes one object in the type + * Deletes one object in the type. * * @param object $object - * @return null **/ public function deleteOne($object) { - $document = $this->transformToElasticaDocument($object); - try { - $this->type->deleteById($document->getId()); - } catch (NotFoundException $e) {} + $this->deleteMany(array($object)); } /** - * Deletes one object in the type by id + * Deletes one object in the type by id. * * @param mixed $id - * - * @return null **/ public function deleteById($id) { - try { - $this->type->deleteById($id); - } catch (NotFoundException $e) {} + $this->deleteManyByIdentifiers(array($id)); } /** - * Bulk insert an array of objects in the type for the given method + * Bulk insert an array of objects in the type for the given method. * * @param array $objects array of domain model objects * @param string Method to call @@ -148,7 +148,7 @@ class ObjectPersister implements ObjectPersisterInterface } /** - * Bulk deletes an array of objects in the type + * Bulk deletes an array of objects in the type. * * @param array $objects array of domain model objects */ @@ -166,7 +166,7 @@ class ObjectPersister implements ObjectPersisterInterface } /** - * Bulk deletes records from an array of identifiers + * Bulk deletes records from an array of identifiers. * * @param array $identifiers array of domain model object identifiers */ @@ -180,9 +180,10 @@ class ObjectPersister implements ObjectPersisterInterface } /** - * Transforms an object to an elastica document + * Transforms an object to an elastica document. * * @param object $object + * * @return Document the elastica document */ public function transformToElasticaDocument($object) diff --git a/Persister/ObjectPersisterInterface.php b/Persister/ObjectPersisterInterface.php index 2b4c8ee..f624971 100644 --- a/Persister/ObjectPersisterInterface.php +++ b/Persister/ObjectPersisterInterface.php @@ -4,66 +4,73 @@ namespace FOS\ElasticaBundle\Persister; /** * Inserts, replaces and deletes single documents in an elastica type - * Accepts domain model objects and converts them to elastica documents + * Accepts domain model objects and converts them to elastica documents. * * @author Thibault Duplessis */ interface ObjectPersisterInterface { + /** + * Checks if this persister can handle the given object or not. + * + * @param mixed $object + * + * @return boolean + */ + public function handlesObject($object); + /** * Insert one object into the type - * The object will be transformed to an elastica document + * The object will be transformed to an elastica document. * * @param object $object */ - function insertOne($object); + public function insertOne($object); /** - * Replaces one object in the type + * Replaces one object in the type. * * @param object $object **/ - function replaceOne($object); + public function replaceOne($object); /** - * Deletes one object in the type + * Deletes one object in the type. * * @param object $object **/ - function deleteOne($object); + public function deleteOne($object); /** - * Deletes one object in the type by id + * Deletes one object in the type by id. * * @param mixed $id - * - * @return null */ - function deleteById($id); + public function deleteById($id); /** - * Bulk inserts an array of objects in the type + * Bulk inserts an array of objects in the type. * * @param array $objects array of domain model objects */ - function insertMany(array $objects); + public function insertMany(array $objects); /** - * Bulk updates an array of objects in the type + * Bulk updates an array of objects in the type. * * @param array $objects array of domain model objects */ - function replaceMany(array $objects); + public function replaceMany(array $objects); /** - * Bulk deletes an array of objects in the type + * Bulk deletes an array of objects in the type. * * @param array $objects array of domain model objects */ - function deleteMany(array $objects); + public function deleteMany(array $objects); /** - * Bulk deletes records from an array of identifiers + * Bulk deletes records from an array of identifiers. * * @param array $identifiers array of domain model object identifiers */ diff --git a/Persister/ObjectSerializerPersister.php b/Persister/ObjectSerializerPersister.php index 1a15656..792aa9a 100644 --- a/Persister/ObjectSerializerPersister.php +++ b/Persister/ObjectSerializerPersister.php @@ -9,7 +9,7 @@ use FOS\ElasticaBundle\Transformer\ModelToElasticaTransformerInterface; /** * Inserts, replaces and deletes single objects in an elastica type, making use * of elastica's serializer support to convert objects in to elastica documents. - * Accepts domain model objects and passes them directly to elastica + * Accepts domain model objects and passes them directly to elastica. * * @author Lea Haensenberber */ @@ -17,17 +17,25 @@ class ObjectSerializerPersister extends ObjectPersister { protected $serializer; + /** + * @param Type $type + * @param ModelToElasticaTransformerInterface $transformer + * @param string $objectClass + * @param callable $serializer + */ public function __construct(Type $type, ModelToElasticaTransformerInterface $transformer, $objectClass, $serializer) { parent::__construct($type, $transformer, $objectClass, array()); - $this->serializer = $serializer; + + $this->serializer = $serializer; } /** * Transforms an object to an elastica document - * with just the identifier set + * with just the identifier set. * * @param object $object + * * @return Document the elastica document */ public function transformToElasticaDocument($object) diff --git a/Propel/ElasticaToModelTransformer.php b/Propel/ElasticaToModelTransformer.php index af5f8ab..d143478 100644 --- a/Propel/ElasticaToModelTransformer.php +++ b/Propel/ElasticaToModelTransformer.php @@ -3,6 +3,7 @@ namespace FOS\ElasticaBundle\Propel; use FOS\ElasticaBundle\HybridResult; +use FOS\ElasticaBundle\Transformer\AbstractElasticaToModelTransformer; use FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -14,7 +15,7 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; * * @author William Durand */ -class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface +class ElasticaToModelTransformer extends AbstractElasticaToModelTransformer { /** * Propel model class to map to Elastica documents. @@ -33,18 +34,11 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface 'identifier' => 'id', ); - /** - * PropertyAccessor instance. - * - * @var PropertyAccessorInterface - */ - protected $propertyAccessor; - /** * Constructor. * * @param string $objectClass - * @param array $options + * @param array $options */ public function __construct($objectClass, array $options = array()) { @@ -52,21 +46,12 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface $this->options = array_merge($this->options, $options); } - /** - * Set the PropertyAccessor instance. - * - * @param PropertyAccessorInterface $propertyAccessor - */ - public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor) - { - $this->propertyAccessor = $propertyAccessor; - } - /** * Transforms an array of Elastica document into an array of Propel entities * fetched from the database. * * @param array $elasticaObjects + * * @return array|\ArrayObject */ public function transform(array $elasticaObjects) @@ -81,11 +66,7 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface // Sort objects in the order of their IDs $idPos = array_flip($ids); $identifier = $this->options['identifier']; - $propertyAccessor = $this->propertyAccessor; - - $sortCallback = function($a, $b) use ($idPos, $identifier, $propertyAccessor) { - return $idPos[$propertyAccessor->getValue($a, $identifier)] > $idPos[$propertyAccessor->getValue($b, $identifier)]; - }; + $sortCallback = $this->getSortingClosure($idPos, $identifier); if (is_object($objects)) { $objects->uasort($sortCallback); @@ -104,7 +85,7 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface $objects = $this->transform($elasticaObjects); $result = array(); - for ($i = 0; $i < count($elasticaObjects); $i++) { + for ($i = 0, $j = count($elasticaObjects); $i < $j; $i++) { $result[] = new HybridResult($elasticaObjects[$i], $objects[$i]); } @@ -135,6 +116,7 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface * * @param array $identifierValues Identifier values * @param boolean $hydrate Whether or not to hydrate the results + * * @return array */ protected function findByIdentifiers(array $identifierValues, $hydrate) @@ -145,7 +127,7 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface $query = $this->createQuery($this->objectClass, $this->options['identifier'], $identifierValues); - if ( ! $hydrate) { + if (! $hydrate) { return $query->toArray(); } @@ -158,6 +140,7 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface * @param string $class Propel model class * @param string $identifierField Identifier field name (e.g. "id") * @param array $identifierValues Identifier values + * * @return \ModelCriteria */ protected function createQuery($class, $identifierField, array $identifierValues) @@ -170,6 +153,8 @@ class ElasticaToModelTransformer implements ElasticaToModelTransformerInterface /** * @see https://github.com/doctrine/common/blob/master/lib/Doctrine/Common/Util/Inflector.php + * + * @param string $str */ private function camelize($str) { diff --git a/Propel/Provider.php b/Propel/Provider.php index 393beba..9864d53 100644 --- a/Propel/Provider.php +++ b/Propel/Provider.php @@ -5,44 +5,69 @@ namespace FOS\ElasticaBundle\Propel; use FOS\ElasticaBundle\Provider\AbstractProvider; /** - * Propel provider + * Propel provider. * * @author William Durand */ class Provider extends AbstractProvider { /** - * @see FOS\ElasticaBundle\Provider\ProviderInterface::populate() + * {@inheritDoc} */ - public function populate(\Closure $loggerClosure = null, array $options = array()) + public function doPopulate($options, \Closure $loggerClosure = null) { - $queryClass = $this->objectClass . 'Query'; + $queryClass = $this->objectClass.'Query'; $nbObjects = $queryClass::create()->count(); - $offset = isset($options['offset']) ? intval($options['offset']) : 0; - $sleep = isset($options['sleep']) ? intval($options['sleep']) : 0; - $batchSize = isset($options['batch-size']) ? intval($options['batch-size']) : $this->options['batch_size']; - for (; $offset < $nbObjects; $offset += $batchSize) { - if ($loggerClosure) { - $stepStartTime = microtime(true); + $offset = $options['offset']; + + for (; $offset < $nbObjects; $offset += $options['batch_size']) { + $objects = $queryClass::create() + ->limit($options['batch_size']) + ->offset($offset) + ->find() + ->getArrayCopy(); + $objects = $this->filterObjects($options, $objects); + if (!empty($objects)) { + $this->objectPersister->insertMany($objects); } - $objects = $queryClass::create() - ->limit($batchSize) - ->offset($offset) - ->find(); - - $this->objectPersister->insertMany($objects->getArrayCopy()); - - usleep($sleep); + usleep($options['sleep']); if ($loggerClosure) { - $stepNbObjects = count($objects); - $stepCount = $stepNbObjects + $offset; - $percentComplete = 100 * $stepCount / $nbObjects; - $objectsPerSecond = $stepNbObjects / (microtime(true) - $stepStartTime); - $loggerClosure(sprintf('%0.1f%% (%d/%d), %d objects/s %s', $percentComplete, $stepCount, $nbObjects, $objectsPerSecond, $this->getMemoryUsage())); + $loggerClosure($options['batch_size'], $nbObjects); } } } + + /** + * {@inheritDoc} + */ + protected function configureOptions() + { + parent::configureOptions(); + + $this->resolver->setDefaults(array( + 'clear_object_manager' => true, + 'debug_logging' => false, + 'ignore_errors' => false, + 'offset' => 0, + 'query_builder_method' => null, + 'sleep' => 0 + )); + } + + /** + * {@inheritDoc} + */ + protected function disableLogging() + { + } + + /** + * {@inheritDoc} + */ + protected function enableLogging($logger) + { + } } diff --git a/Provider/AbstractProvider.php b/Provider/AbstractProvider.php index 2761a25..f05ab98 100644 --- a/Provider/AbstractProvider.php +++ b/Provider/AbstractProvider.php @@ -3,16 +3,17 @@ namespace FOS\ElasticaBundle\Provider; use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; /** - * AbstractProvider + * AbstractProvider. */ abstract class AbstractProvider implements ProviderInterface { /** - * @var ObjectPersisterInterface + * @var array */ - protected $objectPersister; + protected $baseOptions; /** * @var string @@ -20,29 +21,154 @@ abstract class AbstractProvider implements ProviderInterface protected $objectClass; /** - * @var array + * @var ObjectPersisterInterface */ - protected $options; + protected $objectPersister; + + /** + * @var OptionsResolver + */ + protected $resolver; + + /** + * @var IndexableInterface + */ + private $indexable; /** * Constructor. * * @param ObjectPersisterInterface $objectPersister + * @param IndexableInterface $indexable * @param string $objectClass - * @param array $options + * @param array $baseOptions */ - public function __construct(ObjectPersisterInterface $objectPersister, $objectClass, array $options = array()) - { - $this->objectPersister = $objectPersister; + public function __construct( + ObjectPersisterInterface $objectPersister, + IndexableInterface $indexable, + $objectClass, + array $baseOptions = array() + ) { + $this->baseOptions = $baseOptions; + $this->indexable = $indexable; $this->objectClass = $objectClass; - - $this->options = array_merge(array( - 'batch_size' => 100, - ), $options); + $this->objectPersister = $objectPersister; + $this->resolver = new OptionsResolver(); + $this->configureOptions(); } /** - * Get string with RAM usage information (current and peak) + * {@inheritDoc} + */ + public function populate(\Closure $loggerClosure = null, array $options = array()) + { + $options = $this->resolveOptions($options); + + $logger = !$options['debug_logging'] ? + $this->disableLogging() : + null; + + $this->doPopulate($options, $loggerClosure); + + if (null !== $logger) { + $this->enableLogging($logger); + } + } + + /** + * Disables logging and returns the logger that was previously set. + * + * @return mixed + */ + abstract protected function disableLogging(); + + /** + * Perform actual population. + * + * @param array $options + * @param \Closure $loggerClosure + */ + abstract protected function doPopulate($options, \Closure $loggerClosure = null); + + /** + * Reenables the logger with the previously returned logger from disableLogging();. + * + * @param mixed $logger + * + * @return mixed + */ + abstract protected function enableLogging($logger); + + /** + * Configures the option resolver. + */ + protected function configureOptions() + { + $this->resolver->setDefaults(array( + 'batch_size' => 100, + 'skip_indexable_check' => false, + )); + $this->resolver->setAllowedTypes(array( + 'batch_size' => 'int' + )); + + $this->resolver->setRequired(array( + 'indexName', + 'typeName', + )); + } + + + /** + * Filters objects away if they are not indexable. + * + * @param array $options + * @param array $objects + * @return array + */ + protected function filterObjects(array $options, array $objects) + { + if ($options['skip_indexable_check']) { + return $objects; + } + + $index = $options['indexName']; + $type = $options['typeName']; + + $return = array(); + foreach ($objects as $object) { + if (!$this->indexable->isObjectIndexable($index, $type, $object)) { + continue; + } + + $return[] = $object; + } + + return $return; + } + + /** + * Checks if a given object should be indexed or not. + * + * @deprecated To be removed in 4.0 + * + * @param object $object + * + * @return bool + */ + protected function isObjectIndexable($object) + { + return $this->indexable->isObjectIndexable( + $this->baseOptions['indexName'], + $this->baseOptions['typeName'], + $object + ); + } + + /** + * Get string with RAM usage information (current and peak). + * + * @deprecated To be removed in 4.0 * * @return string */ @@ -53,4 +179,17 @@ abstract class AbstractProvider implements ProviderInterface return sprintf('(RAM : current=%uMo peak=%uMo)', $memory, $memoryMax); } + + /** + * Merges the base options provided by the class with options passed to the populate + * method and runs them through the resolver. + * + * @param array $options + * + * @return array + */ + protected function resolveOptions(array $options) + { + return $this->resolver->resolve(array_merge($this->baseOptions, $options)); + } } diff --git a/Provider/Indexable.php b/Provider/Indexable.php new file mode 100644 index 0000000..c26da5a --- /dev/null +++ b/Provider/Indexable.php @@ -0,0 +1,240 @@ + + * + * 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 + * @param ContainerInterface $container + */ + 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 (bool) $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; + } + + $callback = $this->callbacks[$type]; + + if (is_callable($callback) or is_callable(array($object, $callback))) { + return $callback; + } + + if (is_array($callback) && !is_object($callback[0])) { + return $this->processArrayToCallback($type, $callback); + } + + if (is_string($callback)) { + return $this->buildExpressionCallback($type, $object, $callback); + } + + throw new \InvalidArgumentException(sprintf('Callback for type "%s" is not a valid callback.', $type)); + } + + /** + * Processes a string expression into an Expression. + * + * @param string $type + * @param mixed $object + * @param string $callback + * + * @return Expression + */ + private function buildExpressionCallback($type, $object, $callback) + { + $expression = $this->getExpressionLanguage(); + if (!$expression) { + throw new \RuntimeException('Unable to process an expression without the ExpressionLanguage component.'); + } + + try { + $callback = new Expression($callback); + $expression->compile($callback, array( + 'object', $this->getExpressionVar($object) + )); + + return $callback; + } catch (SyntaxError $e) { + throw new \InvalidArgumentException(sprintf( + 'Callback for type "%s" is an invalid expression', + $type + ), $e->getCode(), $e); + } + } + + /** + * Retreives a cached callback, or creates a new callback if one is not found. + * + * @param string $type + * @param object $object + * + * @return mixed + */ + private function getCallback($type, $object) + { + if (!array_key_exists($type, $this->initialisedCallbacks)) { + $this->initialisedCallbacks[$type] = $this->buildCallback($type, $object); + } + + return $this->initialisedCallbacks[$type]; + } + + /** + * Returns the ExpressionLanguage class if it is available. + * + * @return ExpressionLanguage|null + */ + private function getExpressionLanguage() + { + if (null === $this->expressionLanguage && class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + $this->expressionLanguage = new ExpressionLanguage(); + } + + return $this->expressionLanguage; + } + + /** + * Returns the variable name to be used to access the object when using the ExpressionLanguage + * component to parse and evaluate an expression. + * + * @param mixed $object + * + * @return string + */ + private function getExpressionVar($object = null) + { + if (!is_object($object)) { + return 'object'; + } + + $ref = new \ReflectionClass($object); + + return strtolower($ref->getShortName()); + } + + /** + * Processes an array into a callback. Replaces the first element with a service if + * it begins with an @. + * + * @param string $type + * @param array $callback + * @return array + */ + private function processArrayToCallback($type, array $callback) + { + list($class, $method) = $callback + array(null, '__invoke'); + + if (strpos($class, '@') === 0) { + $service = $this->container->get(substr($class, 1)); + $callback = array($service, $method); + + if (!is_callable($callback)) { + throw new \InvalidArgumentException(sprintf( + 'Method "%s" on service "%s" is not callable.', + $method, + substr($class, 1) + )); + } + + return $callback; + } + + throw new \InvalidArgumentException(sprintf( + 'Unable to parse callback array for type "%s"', + $type + )); + } +} diff --git a/Provider/IndexableInterface.php b/Provider/IndexableInterface.php new file mode 100644 index 0000000..0d9f047 --- /dev/null +++ b/Provider/IndexableInterface.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\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/Provider/ProviderInterface.php b/Provider/ProviderInterface.php index e8d7ea4..1ba977b 100644 --- a/Provider/ProviderInterface.php +++ b/Provider/ProviderInterface.php @@ -3,7 +3,7 @@ namespace FOS\ElasticaBundle\Provider; /** - * Insert application domain objects into elastica types + * Insert application domain objects into elastica types. * * @author Thibault Duplessis */ @@ -12,9 +12,15 @@ interface ProviderInterface /** * Persists all domain objects to ElasticSearch for this provider. * + * The closure can expect 2 or 3 arguments: + * * The step size + * * The total number of objects + * * A message to output in error conditions (not normally provided) + * * @param \Closure $loggerClosure * @param array $options + * * @return */ - function populate(\Closure $loggerClosure = null, array $options = array()); + public function populate(\Closure $loggerClosure = null, array $options = array()); } diff --git a/Provider/ProviderRegistry.php b/Provider/ProviderRegistry.php index 2142223..9fc9e3c 100644 --- a/Provider/ProviderRegistry.php +++ b/Provider/ProviderRegistry.php @@ -57,8 +57,10 @@ class ProviderRegistry implements ContainerAwareInterface * * Providers will be indexed by "type" strings in the returned array. * - * @param string $index - * @return array of ProviderInterface instances + * @param string $index + * + * @return ProviderInterface[] + * * @throws \InvalidArgumentException if no providers were registered for the index */ public function getIndexProviders($index) @@ -81,7 +83,9 @@ class ProviderRegistry implements ContainerAwareInterface * * @param string $index * @param string $type + * * @return ProviderInterface + * * @throws \InvalidArgumentException if no provider was registered for the index and type */ public function getProvider($index, $type) diff --git a/README.md b/README.md index 7909d2b..797d629 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,17 @@ Symfony2. Features include: > **Note** Propel support is limited and contributions fixing issues are welcome! -[![Build Status](https://secure.travis-ci.org/FriendsOfSymfony/FOSElasticaBundle.png?branch=master)](http://travis-ci.org/FriendsOfSymfony/FOSElasticaBundle) [![Total Downloads](https://poser.pugx.org/FriendsOfSymfony/elastica-bundle/downloads.png)](https://packagist.org/packages/FriendsOfSymfony/elastica-bundle) [![Latest Stable Version](https://poser.pugx.org/FriendsOfSymfony/elastica-bundle/v/stable.png)](https://packagist.org/packages/FriendsOfSymfony/elastica-bundle) +[![Build Status](https://secure.travis-ci.org/FriendsOfSymfony/FOSElasticaBundle.png?branch=master)](http://travis-ci.org/FriendsOfSymfony/FOSElasticaBundle) [![Total Downloads](https://poser.pugx.org/FriendsOfSymfony/elastica-bundle/downloads.png)](https://packagist.org/packages/FriendsOfSymfony/elastica-bundle) [![Latest Stable Version](https://poser.pugx.org/FriendsOfSymfony/elastica-bundle/v/stable.png)](https://packagist.org/packages/FriendsOfSymfony/elastica-bundle) [![Latest Unstable Version](https://poser.pugx.org/friendsofsymfony/elastica-bundle/v/unstable.svg)](https://packagist.org/packages/friendsofsymfony/elastica-bundle) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/FriendsOfSymfony/FOSElasticaBundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/FriendsOfSymfony/FOSElasticaBundle/?branch=master) Documentation ------------- Documentation for FOSElasticaBundle is in `Resources/doc/index.md` -[Read the documentation for 3.0.x (master)](https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/master/Resources/doc/index.md) +[Read the documentation for 3.1.x](https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/master/Resources/doc/index.md) -[Read the documentation for 2.1.x](https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/2.1.x/README.md) +[Read the documentation for 3.0.x](https://github.com/FriendsOfSymfony/FOSElasticaBundle/blob/3.0.x/Resources/doc/index.md) Installation ------------ diff --git a/Repository.php b/Repository.php index 70b2a21..04a51c5 100644 --- a/Repository.php +++ b/Repository.php @@ -19,21 +19,47 @@ class Repository $this->finder = $finder; } + /** + * @param mixed $query + * @param integer $limit + * @param array $options + * + * @return array + */ public function find($query, $limit = null, $options = array()) { return $this->finder->find($query, $limit, $options); } + /** + * @param mixed $query + * @param integer $limit + * @param array $options + * + * @return mixed + */ public function findHybrid($query, $limit = null, $options = array()) { return $this->finder->findHybrid($query, $limit, $options); } + /** + * @param mixed $query + * @param array $options + * + * @return \Pagerfanta\Pagerfanta + */ public function findPaginated($query, $options = array()) { return $this->finder->findPaginated($query, $options); } + /** + * @param string $query + * @param array $options + * + * @return Paginator\PaginatorAdapterInterface + */ public function createPaginatorAdapter($query, $options = array()) { return $this->finder->createPaginatorAdapter($query, $options); diff --git a/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 7687250..06f0cda 100644 --- a/Resources/config/config.xml +++ b/Resources/config/config.xml @@ -1,92 +1,36 @@ + 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\IndexManager - FOS\ElasticaBundle\Resetter - FOS\ElasticaBundle\Finder\TransformedFinder + 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% - + + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -94,7 +38,14 @@ + + + %kernel.debug% + + + + + - diff --git a/Resources/config/index.xml b/Resources/config/index.xml new file mode 100644 index 0000000..3ae2e50 --- /dev/null +++ b/Resources/config/index.xml @@ -0,0 +1,53 @@ + + + + + + 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..97ed16e 100644 --- a/Resources/config/mongodb.xml +++ b/Resources/config/mongodb.xml @@ -4,23 +4,35 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - + + FOS\ElasticaBundle\Doctrine\MongoDB\SliceFetcher + FOS\ElasticaBundle\Doctrine\MongoDB\Provider + FOS\ElasticaBundle\Doctrine\Listener + FOS\ElasticaBundle\Doctrine\MongoDB\ElasticaToModelTransformer + FOS\ElasticaBundle\Doctrine\RepositoryManager + - + + + + + + - + + - + - - - + + + null - + @@ -29,11 +41,9 @@ - + - - diff --git a/Resources/config/orm.xml b/Resources/config/orm.xml index 5bd16e5..8147d51 100644 --- a/Resources/config/orm.xml +++ b/Resources/config/orm.xml @@ -1,27 +1,38 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + + FOS\ElasticaBundle\Doctrine\ORM\SliceFetcher + FOS\ElasticaBundle\Doctrine\ORM\Provider + FOS\ElasticaBundle\Doctrine\Listener + FOS\ElasticaBundle\Doctrine\ORM\ElasticaToModelTransformer + FOS\ElasticaBundle\Doctrine\RepositoryManager + + + - + + - + + - + - - - - + + + null - + @@ -30,11 +41,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..0957152 --- /dev/null +++ b/Resources/config/transformer.xml @@ -0,0 +1,33 @@ + + + + + + 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-properties.md b/Resources/doc/cookbook/custom-properties.md new file mode 100644 index 0000000..1d7687e --- /dev/null +++ b/Resources/doc/cookbook/custom-properties.md @@ -0,0 +1,33 @@ +##### Custom Properties + +Since FOSElasticaBundle 3.1.0, we now dispatch an event for each transformation of an +object into an Elastica document which allows you to set custom properties on the Elastica +document for indexing. + +Set up an event listener or subscriber for +`FOS\ElasticaBundle\Event\TransformEvent::POST_TRANSFORM` to be able to inject your own +parameters. + +```php +class CustomPropertyListener implements EventSubscriberInterface +{ + private $anotherService; + + // ... + + public function addCustomProperty(TransformEvent $event) + { + $document = $event->getDocument(); + $custom = $this->anotherService->calculateCustom($event->getObject()); + + $document->set('custom', $custom); + } + + public static function getSubscribedEvents() + { + return array( + TransformEvent::POST_TRANSFORM => 'addCustomProperty', + ); + } +} +``` diff --git a/Resources/doc/cookbook/custom-repositories.md b/Resources/doc/cookbook/custom-repositories.md index 47dc3fe..866f72d 100644 --- a/Resources/doc/cookbook/custom-repositories.md +++ b/Resources/doc/cookbook/custom-repositories.md @@ -4,7 +4,7 @@ As well as the default repository you can create a custom repository for an enti methods for particular searches. These need to extend `FOS\ElasticaBundle\Repository` to have access to the finder: -``` +```php get('fos_elastica.manager'); +```php +/** var FOS\ElasticaBundle\Manager\RepositoryManager */ +$repositoryManager = $container->get('fos_elastica.manager'); - /** var FOS\ElasticaBundle\Repository */ - $repository = $repositoryManager->getRepository('UserBundle:User'); +/** var FOS\ElasticaBundle\Repository */ +$repository = $repositoryManager->getRepository('UserBundle:User'); - /** var array of Acme\UserBundle\Entity\User */ - $users = $repository->findWithCustomQuery('bob'); +/** var array of Acme\UserBundle\Entity\User */ +$users = $repository->findWithCustomQuery('bob'); +``` Alternatively you can specify the custom repository using an annotation in the entity: -``` +```php indexableUsername`, and the indexed field `firstName` would be populated from a +key `first` from an array on `User->names`. + +Setting the property path to `false` will disable transformation of that value. In this +case the mapping will be created but no value will be populated while indexing. You can +populate this value by listening to the `POST_TRANSFORM` event emitted by this bundle. +See [cookbook/custom-properties.md](cookbook/custom-properties.md) for more information +about this event. + Handling missing results with FOSElasticaBundle ----------------------------------------------- @@ -149,6 +177,52 @@ analyzer, you could write: title: { boost: 8, analyzer: my_analyzer } ``` +Testing if an object should be indexed +-------------------------------------- + +FOSElasticaBundle can be configured to automatically index changes made for +different kinds of objects if your persistence backend supports these methods, +but in some cases you might want to run an external service or call a property +on the object to see if it should be indexed. + +A property, `indexable_callback` is provided under the type configuration that +lets you configure this behaviour which will apply for any automated watching +for changes and for a repopulation of an index. + +In the example below, we're checking the enabled property on the user to only +index enabled users. + +```yaml + types: + users: + indexable_callback: 'enabled' +``` + +The callback option supports multiple approaches: + +* A method on the object itself provided as a string. `enabled` will call + `Object->enabled()`. Note that this does not support chaining methods with dot notation + like property paths. To achieve something similar use the ExpressionLanguage option + below. +* An array of a service id and a method which will be called with the object as the first + and only argument. `[ @my_custom_service, 'userIndexable' ]` will call the userIndexable + method on a service defined as my_custom_service. +* An array of a class and a static method to call on that class which will be called with + the object as the only argument. `[ 'Acme\DemoBundle\IndexableChecker', 'isIndexable' ]` + will call Acme\DemoBundle\IndexableChecker::isIndexable($object) +* A single element array with a service id can be used if the service has an __invoke + method. Such an invoke method must accept a single parameter for the object to be indexed. + `[ @my_custom_invokable_service ]` +* If you have the ExpressionLanguage component installed, A valid ExpressionLanguage + expression provided as a string. The object being indexed will be supplied as `object` + in the expression. `object.isEnabled() or object.shouldBeIndexedAnyway()`. For more + 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 ---------------------- @@ -234,49 +308,6 @@ You can also choose to only listen for some of the events: > **Propel** doesn't support this feature yet. -### 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: - -```yaml - 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: - -```yaml - 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: - -```yaml - 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. - Flushing Method --------------- diff --git a/Resources/doc/usage.md b/Resources/doc/usage.md index c1d5982..be11dbf 100644 --- a/Resources/doc/usage.md +++ b/Resources/doc/usage.md @@ -26,7 +26,9 @@ $userPaginator = $finder->findPaginated('bob'); $countOfResults = $userPaginator->getNbResults(); // Option 3b. KnpPaginator resultset - +$paginator = $this->get('knp_paginator'); +$results = $finder->createPaginatorAdapter('bob'); +$pagination = $paginator->paginate($results, $page, 10); ``` Faceted Searching @@ -45,7 +47,7 @@ $companies = $finder->findPaginated($query); $companies->setMaxPerPage($params['limit']); $companies->setCurrentPage($params['page']); -$facets = $companies->getAdapter()->getFacets()); +$facets = $companies->getAdapter()->getFacets(); ``` Searching the entire index @@ -65,7 +67,7 @@ You can now use the index wide finder service `fos_elastica.finder.website`: ```php /** var FOS\ElasticaBundle\Finder\MappedFinder */ -$finder = $container->get('fos_elastica.finder.website'); +$finder = $this->container->get('fos_elastica.finder.website'); // Returns a mixed array of any objects mapped $results = $finder->find('bob'); @@ -91,7 +93,7 @@ An example for using a repository: ```php /** var FOS\ElasticaBundle\Manager\RepositoryManager */ -$repositoryManager = $container->get('fos_elastica.manager'); +$repositoryManager = $this->container->get('fos_elastica.manager'); /** var FOS\ElasticaBundle\Repository */ $repository = $repositoryManager->getRepository('UserBundle:User'); @@ -160,7 +162,7 @@ fos_elastica: site: settings: index: - analysis: + analysis: analyzer: my_analyzer: type: snowball @@ -184,7 +186,7 @@ The following code will execute a search against the Elasticsearch server: $finder = $this->container->get('fos_elastica.finder.site.article'); $boolQuery = new \Elastica\Query\Bool(); -$fieldQuery = new \Elastica\Query\Text(); +$fieldQuery = new \Elastica\Query\Match(); $fieldQuery->setFieldQuery('title', 'I am a title string'); $fieldQuery->setFieldParam('title', 'analyzer', 'my_analyzer'); $boolQuery->addShould($fieldQuery); diff --git a/Resources/public/images/elastica.png b/Resources/public/images/elastica.png deleted file mode 100644 index dbde014..0000000 Binary files a/Resources/public/images/elastica.png and /dev/null differ diff --git a/Resources/views/Collector/elastica.html.twig b/Resources/views/Collector/elastica.html.twig index 637dae7..82a3dcf 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 %}
@@ -20,10 +23,11 @@ {% block menu %} - + Elastica {{ collector.querycount }} + {{ '%0.0f'|format(collector.time * 1000) }} ms {% endblock %} diff --git a/Serializer/Callback.php b/Serializer/Callback.php index 9fe7064..61da997 100644 --- a/Serializer/Callback.php +++ b/Serializer/Callback.php @@ -8,7 +8,7 @@ use JMS\Serializer\SerializerInterface; class Callback { protected $serializer; - protected $groups; + protected $groups = array(); protected $version; public function setSerializer($serializer) @@ -23,10 +23,8 @@ class Callback { $this->groups = $groups; - if ($this->groups) { - if (!$this->serializer instanceof SerializerInterface) { - throw new \RuntimeException('Setting serialization groups requires using "JMS\Serializer\Serializer".'); - } + if (!empty($this->groups) && !$this->serializer instanceof SerializerInterface) { + throw new \RuntimeException('Setting serialization groups requires using "JMS\Serializer\Serializer".'); } } @@ -34,18 +32,16 @@ class Callback { $this->version = $version; - if ($this->version) { - if (!$this->serializer instanceof SerializerInterface) { - throw new \RuntimeException('Setting serialization version requires using "JMS\Serializer\Serializer".'); - } + if ($this->version && !$this->serializer instanceof SerializerInterface) { + throw new \RuntimeException('Setting serialization version requires using "JMS\Serializer\Serializer".'); } } public function serialize($object) { - $context = $this->serializer instanceof SerializerInterface ? new SerializationContext() : array(); + $context = $this->serializer instanceof SerializerInterface ? SerializationContext::create()->enableMaxDepthChecks() : array(); - if ($this->groups) { + if (!empty($this->groups)) { $context->setGroups($this->groups); } diff --git a/Subscriber/PaginateElasticaQuerySubscriber.php b/Subscriber/PaginateElasticaQuerySubscriber.php index 0b7cfd6..63f6cd0 100644 --- a/Subscriber/PaginateElasticaQuerySubscriber.php +++ b/Subscriber/PaginateElasticaQuerySubscriber.php @@ -32,13 +32,17 @@ class PaginateElasticaQuerySubscriber implements EventSubscriberInterface if (null != $facets) { $event->setCustomPaginationParameter('facets', $facets); } + $aggregations = $results->getAggregations(); + if (null != $aggregations) { + $event->setCustomPaginationParameter('aggregations', $aggregations); + } $event->stopPropagation(); } } /** - * Adds knp paging sort to query + * Adds knp paging sort to query. * * @param ItemsEvent $event */ @@ -70,7 +74,7 @@ class PaginateElasticaQuerySubscriber implements EventSubscriberInterface public static function getSubscribedEvents() { return array( - 'knp_pager.items' => array('items', 1) + 'knp_pager.items' => array('items', 1), ); } -} \ No newline at end of file +} diff --git a/Tests/Command/ResetCommandTest.php b/Tests/Command/ResetCommandTest.php index b6548aa..d63b380 100644 --- a/Tests/Command/ResetCommandTest.php +++ b/Tests/Command/ResetCommandTest.php @@ -2,7 +2,6 @@ namespace FOS\ElasticaBundle\Tests\Command; - use FOS\ElasticaBundle\Command\ResetCommand; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; @@ -10,8 +9,8 @@ use Symfony\Component\DependencyInjection\Container; class ResetCommandTest extends \PHPUnit_Framework_TestCase { + private $command; private $resetter; - private $indexManager; public function setup() @@ -88,4 +87,4 @@ class ResetCommandTest extends \PHPUnit_Framework_TestCase new NullOutput() ); } -} \ No newline at end of file +} diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php index e2fba93..062db5c 100644 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ b/Tests/DependencyInjection/ConfigurationTest.php @@ -6,7 +6,7 @@ use FOS\ElasticaBundle\DependencyInjection\Configuration; use Symfony\Component\Config\Definition\Processor; /** - * ConfigurationTest + * ConfigurationTest. */ class ConfigurationTest extends \PHPUnit_Framework_TestCase { @@ -22,7 +22,7 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase private function getConfigs(array $configArray) { - $configuration = new Configuration($configArray, true); + $configuration = new Configuration(true); return $this->processor->processConfiguration($configuration, array($configArray)); } @@ -34,7 +34,7 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase $this->assertSame(array( 'clients' => array(), 'indexes' => array(), - 'default_manager' => 'orm' + 'default_manager' => 'orm', ), $configuration); } @@ -46,32 +46,32 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase 'url' => 'http://localhost:9200', ), 'clustered' => array( - 'servers' => array( + 'connections' => array( array( 'url' => 'http://es1:9200', 'headers' => array( - 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' - ) + 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', + ), ), array( 'url' => 'http://es2:9200', 'headers' => array( - 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==' - ) + 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', + ), ), - ) - ) - ) + ), + ), + ), )); $this->assertCount(2, $configuration['clients']); - $this->assertCount(1, $configuration['clients']['default']['servers']); - $this->assertCount(0, $configuration['clients']['default']['servers'][0]['headers']); + $this->assertCount(1, $configuration['clients']['default']['connections']); + $this->assertCount(0, $configuration['clients']['default']['connections'][0]['headers']); - $this->assertCount(2, $configuration['clients']['clustered']['servers']); - $this->assertEquals('http://es2:9200/', $configuration['clients']['clustered']['servers'][1]['url']); - $this->assertCount(1, $configuration['clients']['clustered']['servers'][1]['headers']); - $this->assertEquals('Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', $configuration['clients']['clustered']['servers'][0]['headers'][0]); + $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() @@ -91,17 +91,17 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase ), 'logging_custom' => array( 'url' => 'http://localhost:9200', - 'logger' => 'custom.service' + 'logger' => 'custom.service', ), - ) + ), )); $this->assertCount(4, $configuration['clients']); - $this->assertEquals('fos_elastica.logger', $configuration['clients']['logging_enabled']['servers'][0]['logger']); - $this->assertFalse($configuration['clients']['logging_disabled']['servers'][0]['logger']); - $this->assertEquals('fos_elastica.logger', $configuration['clients']['logging_not_mentioned']['servers'][0]['logger']); - $this->assertEquals('custom.service', $configuration['clients']['logging_custom']['servers'][0]['logger']); + $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() @@ -113,12 +113,12 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase ); $configuration = $this->getConfigs($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 testTypeConfig() { - $configuration = $this->getConfigs(array( + $this->getConfigs(array( 'clients' => array( 'default' => array('url' => 'http://localhost:9200'), ), @@ -131,8 +131,8 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase ), 'serializer' => array( 'groups' => array('Search'), - 'version' => 1 - ) + 'version' => 1, + ), ), 'types' => array( 'test' => array( @@ -144,79 +144,122 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase 'persistence' => array( 'listener' => array( 'logger' => true, - ) - ) + ), + ), ), 'test2' => array( 'mappings' => array( 'title' => null, 'children' => array( 'type' => 'nested', - ) - ) - ) - ) - ) - ) + ), + ), + ), + ), + ), + ), )); - - $this->assertEquals('string', $configuration['indexes']['test']['types']['test']['mappings']['title']['type']); - $this->assertTrue($configuration['indexes']['test']['types']['test']['mappings']['title']['include_in_all']); } - public function testEmptyPropertiesIndexIsUnset() + public function testClientConfigurationNoUrl() { - $config = array( + $configuration = $this->getConfigs(array( + 'clients' => array( + 'default' => array( + 'host' => 'localhost', + 'port' => 9200, + ), + ), + )); + + $this->assertTrue(empty($configuration['clients']['default']['connections'][0]['url'])); + } + + 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 testUnconfiguredType() + { + $configuration = $this->getConfigs(array( + 'clients' => array( + 'default' => array('url' => 'http://localhost:9200'), + ), + 'indexes' => array( + 'test' => array( + 'types' => array( + 'test' => null, + ), + ), + ), + )); + + $this->assertArrayHasKey('properties', $configuration['indexes']['test']['types']['test']); + } + + public function testNestedProperties() + { + $this->getConfigs(array( + 'clients' => array( + 'default' => array('url' => 'http://localhost:9200'), + ), 'indexes' => array( 'test' => array( 'types' => array( - 'test' => array( - 'mappings' => array( - 'title' => array( - 'type' => 'string', - 'fields' => array( - 'autocomplete' => null - ) - ), - 'content' => null, - 'children' => array( - 'type' => 'object', + '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( - 'title' => array( - 'type' => 'string', - 'fields' => array( - 'autocomplete' => null - ) + 'nested_field1' => array( + 'type' => 'integer', ), - 'content' => null, - 'tags' => array( + 'nested_field2' => array( + 'type' => 'object', 'properties' => array( - 'tag' => array( - 'type' => 'string', - 'index' => 'not_analyzed' - ) - ) - ) - ) + 'id' => array( + 'type' => 'integer', + ), + ), + ), + ), ), - ) - ) - ) - ) - ) - ); - - $processor = new Processor(); - - $configuration = $processor->processConfiguration(new Configuration(array($config), false), array($config)); - - $mapping = $configuration['indexes']['test']['types']['test']['mappings']; - $this->assertArrayNotHasKey('properties', $mapping['content']); - $this->assertArrayNotHasKey('properties', $mapping['title']); - $this->assertArrayHasKey('properties', $mapping['children']); - $this->assertArrayNotHasKey('properties', $mapping['children']['properties']['title']); - $this->assertArrayNotHasKey('properties', $mapping['children']['properties']['content']); - $this->assertArrayHasKey('properties', $mapping['children']['properties']['tags']); - $this->assertArrayNotHasKey('properties', $mapping['children']['properties']['tags']['properties']['tag']); + ), + ), + ), + ), + ), + )); } } diff --git a/Tests/DependencyInjection/FOSElasticaExtensionTest.php b/Tests/DependencyInjection/FOSElasticaExtensionTest.php new file mode 100644 index 0000000..1bef2b6 --- /dev/null +++ b/Tests/DependencyInjection/FOSElasticaExtensionTest.php @@ -0,0 +1,32 @@ +setParameter('kernel.debug', true); + + $extension = new FOSElasticaExtension(); + + $extension->load($config, $containerBuilder); + + $this->assertTrue($containerBuilder->hasDefinition('fos_elastica.object_persister.test_index.child_field')); + + $persisterCallDefinition = $containerBuilder->getDefinition('fos_elastica.object_persister.test_index.child_field'); + + $arguments = $persisterCallDefinition->getArguments(); + $arguments = $arguments['index_3']; + + $this->assertArrayHasKey('_parent', $arguments); + $this->assertEquals('parent_field', $arguments['_parent']['type']); + } +} diff --git a/Tests/DependencyInjection/fixtures/config.yml b/Tests/DependencyInjection/fixtures/config.yml new file mode 100644 index 0000000..5528d18 --- /dev/null +++ b/Tests/DependencyInjection/fixtures/config.yml @@ -0,0 +1,21 @@ +fos_elastica: + clients: + default: + url: http://localhost:9200 + indexes: + test_index: + client: default + types: + parent_field: + mappings: + text: ~ + persistence: + driver: orm + model: foo_model + child_field: + mappings: + text: ~ + persistence: + driver: orm + model: foo_model + _parent: { type: "parent_field", property: "parent" } diff --git a/Tests/Doctrine/AbstractElasticaToModelTransformerTest.php b/Tests/Doctrine/AbstractElasticaToModelTransformerTest.php new file mode 100644 index 0000000..1185e74 --- /dev/null +++ b/Tests/Doctrine/AbstractElasticaToModelTransformerTest.php @@ -0,0 +1,62 @@ +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() + { + $this->registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry') + ->disableOriginalConstructor() + ->getMock(); + } +} diff --git a/Tests/Doctrine/AbstractListenerTest.php b/Tests/Doctrine/AbstractListenerTest.php index ee657f1..dcaf7d6 100644 --- a/Tests/Doctrine/AbstractListenerTest.php +++ b/Tests/Doctrine/AbstractListenerTest.php @@ -3,7 +3,7 @@ namespace FOS\ElasticaBundle\Tests\Doctrine; /** - * See concrete MongoDB/ORM instances of this abstract test + * See concrete MongoDB/ORM instances of this abstract test. * * @author Richard Miller */ @@ -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, $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, $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, $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,8 +89,7 @@ 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, $indexable, array('indexName' => 'index', 'typeName' => 'type')); $listener->postUpdate($eventArgs); $this->assertEmpty($listener->scheduledForUpdate); @@ -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,7 +124,7 @@ 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, $indexable, array('indexName' => 'index', 'typeName' => 'type')); $listener->preRemove($eventArgs); $this->assertEquals($entity->getId(), current($listener->scheduledForDeletion)); @@ -146,11 +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') @@ -162,7 +157,7 @@ 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, $indexable, array('identifier' => 'identifier', 'indexName' => 'index', 'typeName' => 'type')); $listener->preRemove($eventArgs); $this->assertEquals($entity->identifier, current($listener->scheduledForDeletion)); @@ -174,42 +169,18 @@ abstract class ListenerTest extends \PHPUnit_Framework_TestCase $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() @@ -240,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; } } @@ -251,33 +272,18 @@ namespace FOS\ElasticaBundle\Tests\Doctrine\Listener; class Entity { private $id; - private $isIndexable; + public $identifier; - 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 dcceccf..aa28a4c 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,25 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase private $objectPersister; private $options; private $managerRegistry; + private $indexable; + private $sliceFetcher; public function setUp() { - if (!interface_exists('Doctrine\Common\Persistence\ManagerRegistry')) { - $this->markTestSkipped('Doctrine Common is not available.'); - } + $this->objectClass = 'objectClass'; + $this->options = array('debug_logging' => true, 'indexName' => 'index', 'typeName' => 'type'); - $this->objectClass = 'objectClass'; - $this->options = array('debug_logging' => true); + $this->objectPersister = $this->getMockObjectPersister(); + $this->managerRegistry = $this->getMockManagerRegistry(); + $this->objectManager = $this->getMockObjectManager(); + $this->indexable = $this->getMockIndexable(); - $this->objectPersister = $this->getMockObjectPersister(); - $this->managerRegistry = $this->getMockManagerRegistry(); - $this->objectManager = $this->getMockObjectManager(); + $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)); + $this->sliceFetcher = $this->getMockSliceFetcher(); } /** @@ -49,6 +53,58 @@ 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)); + + $previousSlice = array(); + + foreach ($objectsByIteration as $i => $objects) { + $offset = $objects[0] - 1; + + $this->sliceFetcher->expects($this->at($i)) + ->method('fetch') + ->with($queryBuilder, $batchSize, $offset, $previousSlice, array('id')) + ->will($this->returnValue($objects)); + + $this->objectManager->expects($this->at($i)) + ->method('clear'); + + $previousSlice = $objects; + } + + $this->objectPersister->expects($this->exactly(count($objectsByIteration))) + ->method('insertMany'); + + $provider->populate(); + } + + /** + * @dataProvider providePopulateIterations + */ + public function testPopulateIterationsWithoutSliceFetcher($nbObjects, $objectsByIteration, $batchSize) + { + $this->options['batch_size'] = $batchSize; + + $provider = $this->getMockAbstractProvider(false); + + $queryBuilder = new \stdClass(); + + $provider->expects($this->once()) + ->method('createQueryBuilder') + ->will($this->returnValue($queryBuilder)); + + $provider->expects($this->once()) + ->method('countObjects') + ->with($queryBuilder) + ->will($this->returnValue($nbObjects)); + + $this->indexable->expects($this->any()) + ->method('isObjectIndexable') + ->with('index', 'type', $this->anything()) + ->will($this->returnValue(true)); + $providerInvocationOffset = 2; foreach ($objectsByIteration as $i => $objects) { @@ -59,14 +115,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(); } @@ -75,7 +130,7 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase return array( array( 100, - array(range(1,100)), + array(range(1, 100)), 100, ), array( @@ -98,16 +153,47 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->method('countObjects') ->will($this->returnValue($nbObjects)); - $provider->expects($this->any()) - ->method('fetchSlice') + $this->sliceFetcher->expects($this->any()) + ->method('fetch') ->will($this->returnValue($objects)); + $this->indexable->expects($this->any()) + ->method('isObjectIndexable') + ->with('index', 'type', $this->anything()) + ->will($this->returnValue(true)); + $this->objectManager->expects($this->never()) ->method('clear'); $provider->populate(); } + public function testPopulateShouldClearObjectManagerForFilteredBatch() + { + $nbObjects = 1; + $objects = array(1); + + $provider = $this->getMockAbstractProvider(true); + + $provider->expects($this->any()) + ->method('countObjects') + ->will($this->returnValue($nbObjects)); + + $this->sliceFetcher->expects($this->any()) + ->method('fetch') + ->will($this->returnValue($objects)); + + $this->indexable->expects($this->any()) + ->method('isObjectIndexable') + ->with('index', 'type', $this->anything()) + ->will($this->returnValue(false)); + + $this->objectManager->expects($this->once()) + ->method('clear'); + + $provider->populate(); + } + public function testPopulateInvokesLoggerClosure() { $nbObjects = 1; @@ -119,10 +205,15 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->method('countObjects') ->will($this->returnValue($nbObjects)); - $provider->expects($this->any()) - ->method('fetchSlice') + $this->sliceFetcher->expects($this->any()) + ->method('fetch') ->will($this->returnValue($objects)); + $this->indexable->expects($this->any()) + ->method('isObjectIndexable') + ->with('index', 'type', $this->anything()) + ->will($this->returnValue(true)); + $loggerClosureInvoked = false; $loggerClosure = function () use (&$loggerClosureInvoked) { $loggerClosureInvoked = true; @@ -146,29 +237,68 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase ->method('countObjects') ->will($this->returnValue($nbObjects)); - $provider->expects($this->any()) - ->method('fetchSlice') + $this->sliceFetcher->expects($this->any()) + ->method('fetch') ->will($this->returnValue($objects)); + $this->indexable->expects($this->any()) + ->method('isObjectIndexable') + ->with('index', 'type', $this->anything()) + ->will($this->returnValue(true)); + $this->objectPersister->expects($this->any()) ->method('insertMany') ->will($this->throwException($this->getMockBulkResponseException())); $this->setExpectedException('Elastica\Exception\Bulk\ResponseException'); - $provider->populate(null, array('ignore-errors' => false)); + $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)); + + $this->sliceFetcher->expects($this->any()) + ->method('fetch') + ->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(2)); + + $provider->populate(); } /** + * @param boolean $setSliceFetcher Whether or not to set the slice fetcher. + * * @return \FOS\ElasticaBundle\Doctrine\AbstractProvider|\PHPUnit_Framework_MockObject_MockObject */ - private function getMockAbstractProvider() + private function getMockAbstractProvider($setSliceFetcher = true) { return $this->getMockForAbstractClass('FOS\ElasticaBundle\Doctrine\AbstractProvider', array( $this->objectPersister, + $this->indexable, $this->objectClass, $this->options, $this->managerRegistry, + $setSliceFetcher ? $this->sliceFetcher : null )); } @@ -177,9 +307,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()), + )); } /** @@ -195,7 +325,17 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase */ private function getMockObjectManager() { - return $this->getMock(__NAMESPACE__ . '\ObjectManager'); + $mock = $this->getMock(__NAMESPACE__.'\ObjectManager'); + + $mock->expects($this->any()) + ->method('getClassMetadata') + ->will($this->returnSelf()); + + $mock->expects($this->any()) + ->method('getIdentifierFieldNames') + ->will($this->returnValue(array('id'))); + + return $mock; } /** @@ -205,6 +345,22 @@ 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'); + } + + /** + * @return \FOS\ElasticaBundle\Doctrine\SliceFetcherInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private function getMockSliceFetcher() + { + return $this->getMock('FOS\ElasticaBundle\Doctrine\SliceFetcherInterface'); + } } /** @@ -213,5 +369,7 @@ class AbstractProviderTest extends \PHPUnit_Framework_TestCase */ interface ObjectManager { - function clear(); + public function clear(); + public function getClassMetadata(); + public function getIdentifierFieldNames(); } diff --git a/Tests/Doctrine/ORM/ElasticaToModelTransformerTest.php b/Tests/Doctrine/ORM/ElasticaToModelTransformerTest.php index 14f3ffb..607aeef 100644 --- a/Tests/Doctrine/ORM/ElasticaToModelTransformerTest.php +++ b/Tests/Doctrine/ORM/ElasticaToModelTransformerTest.php @@ -82,13 +82,6 @@ class ElasticaToModelTransformerTest extends \PHPUnit_Framework_TestCase protected function setUp() { - if (!interface_exists('Doctrine\Common\Persistence\ManagerRegistry')) { - $this->markTestSkipped('Doctrine Common is not present'); - } - if (!class_exists('Doctrine\ORM\EntityManager')) { - $this->markTestSkipped('Doctrine Common is not present'); - } - $this->registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry') ->disableOriginalConstructor() ->getMock(); @@ -109,7 +102,7 @@ class ElasticaToModelTransformerTest extends \PHPUnit_Framework_TestCase 'findAll', 'findBy', 'findOneBy', - 'getClassName' + 'getClassName', )); $this->manager->expects($this->any()) diff --git a/Tests/Doctrine/ORM/ListenerTest.php b/Tests/Doctrine/ORM/ListenerTest.php index 12a89b2..36cacc6 100644 --- a/Tests/Doctrine/ORM/ListenerTest.php +++ b/Tests/Doctrine/ORM/ListenerTest.php @@ -6,13 +6,6 @@ use FOS\ElasticaBundle\Tests\Doctrine\ListenerTest as BaseListenerTest; class ListenerTest extends BaseListenerTest { - public function setUp() - { - if (!class_exists('Doctrine\ORM\EntityManager')) { - $this->markTestSkipped('Doctrine ORM is not available.'); - } - } - protected function getClassMetadataClass() { return 'Doctrine\ORM\Mapping\ClassMetadata'; diff --git a/Tests/Doctrine/RepositoryManagerTest.php b/Tests/Doctrine/RepositoryManagerTest.php index ce7b14b..39f9c34 100644 --- a/Tests/Doctrine/RepositoryManagerTest.php +++ b/Tests/Doctrine/RepositoryManagerTest.php @@ -4,22 +4,19 @@ namespace FOS\ElasticaBundle\Tests\Doctrine; use FOS\ElasticaBundle\Doctrine\RepositoryManager; -class CustomRepository{} +class CustomRepository +{ +} -class Entity{} +class Entity +{ +} /** * @author Richard Miller */ class RepositoryManagerTest extends \PHPUnit_Framework_TestCase { - public function setUp() - { - if (!interface_exists('Doctrine\Common\Persistence\ManagerRegistry')) { - $this->markTestSkipped('Doctrine Common is not available.'); - } - } - public function testThatGetRepositoryReturnsDefaultRepository() { /** @var $finderMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ diff --git a/Tests/ClientTest.php b/Tests/Elastica/ClientTest.php similarity index 88% rename from Tests/ClientTest.php rename to Tests/Elastica/ClientTest.php index 8a9d91a..158b553 100644 --- a/Tests/ClientTest.php +++ b/Tests/Elastica/ClientTest.php @@ -1,6 +1,6 @@ getMock('Elastica\Connection'); $connection->expects($this->any())->method('getTransportObject')->will($this->returnValue($transport)); @@ -28,7 +28,7 @@ class ClientTest extends \PHPUnit_Framework_TestCase $this->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..c9513db 100644 --- a/Tests/FOSElasticaBundleTest.php +++ b/Tests/FOSElasticaBundleTest.php @@ -3,37 +3,20 @@ 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]); - - $container - ->expects($this->at(1)) - ->method('addCompilerPass') - ->with($this->isInstanceOf($passes[1][0])); + ->with($this->isInstanceOf('Symfony\\Component\\DependencyInjection\\Compiler\\CompilerPassInterface')); $bundle = new FOSElasticaBundle(); - $bundle->build($container); } } diff --git a/Tests/Functional/ClientTest.php b/Tests/Functional/ClientTest.php new file mode 100644 index 0000000..8a6357a --- /dev/null +++ b/Tests/Functional/ClientTest.php @@ -0,0 +1,48 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace FOS\ElasticaBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Client; + +/** + * @group functional + */ +class ClientTest extends WebTestCase +{ + public function testContainerSource() + { + $client = $this->createClient(array('test_case' => 'Basic')); + + $es = $client->getContainer()->get('fos_elastica.client.default'); + $this->assertInstanceOf('Elastica\\Connection\\Strategy\\RoundRobin', $es->getConnectionStrategy()); + + $es = $client->getContainer()->get('fos_elastica.client.second_server'); + $this->assertInstanceOf('Elastica\\Connection\\Strategy\\RoundRobin', $es->getConnectionStrategy()); + + $es = $client->getContainer()->get('fos_elastica.client.third'); + $this->assertInstanceOf('Elastica\\Connection\\Strategy\\Simple', $es->getConnectionStrategy()); + } + + protected function setUp() + { + parent::setUp(); + + $this->deleteTmpDir('Basic'); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->deleteTmpDir('Basic'); + } +} diff --git a/Tests/Functional/ConfigurationManagerTest.php b/Tests/Functional/ConfigurationManagerTest.php new file mode 100644 index 0000000..a6028b7 --- /dev/null +++ b/Tests/Functional/ConfigurationManagerTest.php @@ -0,0 +1,59 @@ + + * + * 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..3f84286 --- /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..6f93b7e --- /dev/null +++ b/Tests/Functional/MappingToElasticaTest.php @@ -0,0 +1,140 @@ + + * + * 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']); + + $type = $this->getType($client, 'type'); + $mapping = $type->getMapping(); + $this->assertEquals('parent', $mapping['type']['_parent']['type']); + + $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->assertFalse($mapping['type']['date_detection']); + $this->assertTrue($mapping['type']['numeric_detection']); + $this->assertEquals(array('yyyy-MM-dd'), $mapping['type']['dynamic_date_formats']); + $this->assertArrayHasKey('store', $mapping['type']['properties']['field1']); + $this->assertTrue($mapping['type']['properties']['field1']['store']); + $this->assertArrayNotHasKey('store', $mapping['type']['properties']['field2']); + } + + 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 + * @param string $type + * + * @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/PropertyPathTest.php b/Tests/Functional/PropertyPathTest.php new file mode 100644 index 0000000..860cb86 --- /dev/null +++ b/Tests/Functional/PropertyPathTest.php @@ -0,0 +1,54 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace FOS\ElasticaBundle\Tests\Functional; + +use Elastica\Query\Match; + +/** + * @group functional + */ +class PropertyPathTest extends WebTestCase +{ + public function testContainerSource() + { + $client = $this->createClient(array('test_case' => 'ORM')); + /** @var \FOS\ElasticaBundle\Persister\ObjectPersister $persister */ + $persister = $client->getContainer()->get('fos_elastica.object_persister.index.property_paths_type'); + $obj = new TypeObj(); + $obj->coll = 'Hello'; + $persister->insertOne($obj); + + /** @var \Elastica\Index $elClient */ + $index = $client->getContainer()->get('fos_elastica.index.index'); + $index->flush(true); + + $query = new Match(); + $query->setField('something', 'Hello'); + $search = $index->createSearch($query); + + $this->assertEquals(1, $search->count()); + } + + protected function setUp() + { + parent::setUp(); + + $this->deleteTmpDir('Basic'); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->deleteTmpDir('Basic'); + } +} diff --git a/Tests/Functional/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..39e9fe9 --- /dev/null +++ b/Tests/Functional/TypeObj.php @@ -0,0 +1,35 @@ + + * + * 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 $id = 5; + public $coll; + public $field1; + public $field2; + + 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..d75910a --- /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; + } +} 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() + { + return true; + } + + public static function isntIndexable() + { + 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 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Tests\Index; + +use Elastica\Exception\ResponseException; +use Elastica\Request; +use Elastica\Response; +use FOS\ElasticaBundle\Configuration\IndexConfig; +use FOS\ElasticaBundle\Index\AliasProcessor; + +class AliasProcessorTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var AliasProcessor + */ + private $processor; + + /** + * @dataProvider getSetRootNameData + * @param string $name + * @param array $configArray + * @param string $resultStartsWith + */ + public function testSetRootName($name, $configArray, $resultStartsWith) + { + $indexConfig = new IndexConfig($name, array(), $configArray); + $index = $this->getMockBuilder('FOS\\ElasticaBundle\\Elastica\\Index') + ->disableOriginalConstructor() + ->getMock(); + $index->expects($this->once()) + ->method('overrideName') + ->with($this->stringStartsWith($resultStartsWith)); + + $this->processor->setRootName($indexConfig, $index); + } + + public function testSwitchAliasNoAliasSet() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array())); + $client->expects($this->at(1)) + ->method('request') + ->with('_aliases', 'POST', array('actions' => array( + array('add' => array('index' => 'unique_name', 'alias' => 'name')) + ))); + + $this->processor->switchIndexAlias($indexConfig, $index, false); + } + + public function testSwitchAliasExistingAliasSet() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array( + 'old_unique_name' => array('aliases' => array('name')) + ))); + $client->expects($this->at(1)) + ->method('request') + ->with('_aliases', 'POST', array('actions' => array( + array('remove' => array('index' => 'old_unique_name', 'alias' => 'name')), + array('add' => array('index' => 'unique_name', 'alias' => 'name')) + ))); + + $this->processor->switchIndexAlias($indexConfig, $index, false); + } + + /** + * @expectedException \RuntimeException + */ + public function testSwitchAliasThrowsWhenMoreThanOneExists() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array( + 'old_unique_name' => array('aliases' => array('name')), + 'another_old_unique_name' => array('aliases' => array('name')) + ))); + + $this->processor->switchIndexAlias($indexConfig, $index, false); + } + + /** + * @expectedException \FOS\ElasticaBundle\Exception\AliasIsIndexException + */ + public function testSwitchAliasThrowsWhenAliasIsAnIndex() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array( + 'name' => array(), + ))); + + $this->processor->switchIndexAlias($indexConfig, $index, false); + } + + public function testSwitchAliasDeletesIndexCollisionIfForced() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array( + 'name' => array(), + ))); + $client->expects($this->at(1)) + ->method('request') + ->with('name', 'DELETE'); + + $this->processor->switchIndexAlias($indexConfig, $index, true); + } + + public function testSwitchAliasDeletesOldIndex() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array( + 'old_unique_name' => array('aliases' => array('name')), + ))); + $client->expects($this->at(1)) + ->method('request') + ->with('_aliases', 'POST', array('actions' => array( + array('remove' => array('index' => 'old_unique_name', 'alias' => 'name')), + array('add' => array('index' => 'unique_name', 'alias' => 'name')) + ))); + $client->expects($this->at(2)) + ->method('request') + ->with('old_unique_name', 'DELETE'); + + $this->processor->switchIndexAlias($indexConfig, $index, true); + } + + public function testSwitchAliasCleansUpOnRenameFailure() + { + $indexConfig = new IndexConfig('name', array(), array()); + list($index, $client) = $this->getMockedIndex('unique_name'); + + $client->expects($this->at(0)) + ->method('request') + ->with('_aliases', 'GET') + ->willReturn(new Response(array( + 'old_unique_name' => array('aliases' => array('name')), + ))); + $client->expects($this->at(1)) + ->method('request') + ->with('_aliases', 'POST', array('actions' => array( + array('remove' => array('index' => 'old_unique_name', 'alias' => 'name')), + array('add' => array('index' => 'unique_name', 'alias' => 'name')) + ))) + ->will($this->throwException(new ResponseException(new Request(''), new Response('')))); + $client->expects($this->at(2)) + ->method('request') + ->with('unique_name', 'DELETE'); + // Not an annotation: we do not want a RuntimeException until now. + $this->setExpectedException('RuntimeException'); + + $this->processor->switchIndexAlias($indexConfig, $index, true); + } + + public function getSetRootNameData() + { + return array( + array('name', array(), 'name_'), + array('name', array('elasticSearchName' => 'notname'), 'notname_') + ); + } + + protected function setUp() + { + $this->processor = new AliasProcessor(); + } + + private function getMockedIndex($name) + { + $index = $this->getMockBuilder('FOS\\ElasticaBundle\\Elastica\\Index') + ->disableOriginalConstructor() + ->getMock(); + + $client = $this->getMockBuilder('Elastica\\Client') + ->disableOriginalConstructor() + ->getMock(); + $index->expects($this->any()) + ->method('getClient') + ->willReturn($client); + + $index->expects($this->any()) + ->method('getName') + ->willReturn($name); + + return array($index, $client); + } +} diff --git a/Tests/Index/IndexManagerTest.php b/Tests/Index/IndexManagerTest.php new file mode 100644 index 0000000..78a3d28 --- /dev/null +++ b/Tests/Index/IndexManagerTest.php @@ -0,0 +1,58 @@ +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/Index/ResetterTest.php b/Tests/Index/ResetterTest.php new file mode 100644 index 0000000..9b4cd05 --- /dev/null +++ b/Tests/Index/ResetterTest.php @@ -0,0 +1,274 @@ +mockIndex($indexName, $indexConfig); + + $this->configManager->expects($this->once()) + ->method('getIndexNames') + ->will($this->returnValue(array($indexName))); + + $this->dispatcherExpects(array( + array(IndexResetEvent::PRE_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')), + array(IndexResetEvent::POST_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')) + )); + + $this->elasticaClient->expects($this->exactly(2)) + ->method('request') + ->withConsecutive( + array('index1/', 'DELETE'), + array('index1/', 'PUT', array(), array()) + ); + + $this->resetter->resetAllIndexes(); + } + + public function testResetIndex() + { + $indexConfig = new IndexConfig('index1', array(), array()); + $this->mockIndex('index1', $indexConfig); + + $this->dispatcherExpects(array( + array(IndexResetEvent::PRE_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')), + array(IndexResetEvent::POST_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')) + )); + + $this->elasticaClient->expects($this->exactly(2)) + ->method('request') + ->withConsecutive( + array('index1/', 'DELETE'), + array('index1/', 'PUT', array(), array()) + ); + + $this->resetter->resetIndex('index1'); + } + + public function testResetIndexWithDifferentName() + { + $indexConfig = new IndexConfig('index1', array(), array( + 'elasticSearchName' => 'notIndex1' + )); + $this->mockIndex('index1', $indexConfig); + $this->dispatcherExpects(array( + array(IndexResetEvent::PRE_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')), + array(IndexResetEvent::POST_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')) + )); + + $this->elasticaClient->expects($this->exactly(2)) + ->method('request') + ->withConsecutive( + array('index1/', 'DELETE'), + array('index1/', 'PUT', array(), array()) + ); + + $this->resetter->resetIndex('index1'); + } + + public function testResetIndexWithDifferentNameAndAlias() + { + $indexConfig = new IndexConfig('index1', array(), array( + 'elasticSearchName' => 'notIndex1', + 'useAlias' => true + )); + $index = $this->mockIndex('index1', $indexConfig); + $this->dispatcherExpects(array( + array(IndexResetEvent::PRE_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')), + array(IndexResetEvent::POST_INDEX_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\IndexResetEvent')) + )); + + $this->aliasProcessor->expects($this->once()) + ->method('switchIndexAlias') + ->with($indexConfig, $index, false); + + $this->elasticaClient->expects($this->exactly(2)) + ->method('request') + ->withConsecutive( + array('index1/', 'DELETE'), + array('index1/', 'PUT', array(), array()) + ); + + $this->resetter->resetIndex('index1'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testFailureWhenMissingIndexDoesntDispatch() + { + $this->configManager->expects($this->once()) + ->method('getIndexConfiguration') + ->with('nonExistant') + ->will($this->throwException(new \InvalidArgumentException)); + + $this->indexManager->expects($this->never()) + ->method('getIndex'); + + $this->resetter->resetIndex('nonExistant'); + } + + public function testResetType() + { + $typeConfig = new TypeConfig('type', array(), array()); + $this->mockType('type', 'index', $typeConfig); + + $this->dispatcherExpects(array( + array(TypeResetEvent::PRE_TYPE_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\TypeResetEvent')), + array(TypeResetEvent::POST_TYPE_RESET, $this->isInstanceOf('FOS\\ElasticaBundle\\Event\\TypeResetEvent')) + )); + + $this->elasticaClient->expects($this->exactly(2)) + ->method('request') + ->withConsecutive( + array('index/type/', 'DELETE'), + array('index/type/_mapping', 'PUT', array('type' => array()), array()) + ); + + $this->resetter->resetIndexType('index', 'type'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testNonExistantResetType() + { + $this->configManager->expects($this->once()) + ->method('getTypeConfiguration') + ->with('index', 'nonExistant') + ->will($this->throwException(new \InvalidArgumentException)); + + $this->indexManager->expects($this->never()) + ->method('getIndex'); + + $this->resetter->resetIndexType('index', 'nonExistant'); + } + + public function testPostPopulateWithoutAlias() + { + $this->mockIndex('index', new IndexConfig('index', array(), array())); + + $this->indexManager->expects($this->never()) + ->method('getIndex'); + $this->aliasProcessor->expects($this->never()) + ->method('switchIndexAlias'); + + $this->resetter->postPopulate('index'); + } + + public function testPostPopulate() + { + $indexConfig = new IndexConfig('index', array(), array( 'useAlias' => true)); + $index = $this->mockIndex('index', $indexConfig); + + $this->aliasProcessor->expects($this->once()) + ->method('switchIndexAlias') + ->with($indexConfig, $index); + + $this->resetter->postPopulate('index'); + } + + private function dispatcherExpects(array $events) + { + $expectation = $this->dispatcher->expects($this->exactly(count($events))) + ->method('dispatch'); + + call_user_func_array(array($expectation, 'withConsecutive'), $events); + } + + private function mockIndex($indexName, IndexConfig $config, $mapping = array()) + { + $this->configManager->expects($this->atLeast(1)) + ->method('getIndexConfiguration') + ->with($indexName) + ->will($this->returnValue($config)); + $index = new Index($this->elasticaClient, $indexName); + $this->indexManager->expects($this->any()) + ->method('getIndex') + ->with($indexName) + ->willReturn($index); + $this->mappingBuilder->expects($this->any()) + ->method('buildIndexMapping') + ->with($config) + ->willReturn($mapping); + + return $index; + } + + private function mockType($typeName, $indexName, TypeConfig $config, $mapping = array()) + { + $this->configManager->expects($this->atLeast(1)) + ->method('getTypeConfiguration') + ->with($indexName, $typeName) + ->will($this->returnValue($config)); + $index = new Index($this->elasticaClient, $indexName); + $this->indexManager->expects($this->once()) + ->method('getIndex') + ->with($indexName) + ->willReturn($index); + $this->mappingBuilder->expects($this->once()) + ->method('buildTypeMapping') + ->with($config) + ->willReturn($mapping); + + return $index; + } + + protected function setUp() + { + $this->aliasProcessor = $this->getMockBuilder('FOS\\ElasticaBundle\\Index\\AliasProcessor') + ->disableOriginalConstructor() + ->getMock(); + $this->configManager = $this->getMockBuilder('FOS\\ElasticaBundle\\Configuration\\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + $this->dispatcher = $this->getMockBuilder('Symfony\\Component\\EventDispatcher\\EventDispatcherInterface') + ->getMock(); + $this->elasticaClient = $this->getMockBuilder('Elastica\\Client') + ->disableOriginalConstructor() + ->getMock(); + $this->indexManager = $this->getMockBuilder('FOS\\ElasticaBundle\\Index\\IndexManager') + ->disableOriginalConstructor() + ->getMock(); + $this->mappingBuilder = $this->getMockBuilder('FOS\\ElasticaBundle\\Index\\MappingBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $this->resetter = new Resetter( + $this->configManager, + $this->indexManager, + $this->aliasProcessor, + $this->mappingBuilder, + $this->dispatcher + ); + } +} 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..ae7e409 --- /dev/null +++ b/Tests/Integration/MappingTest.php @@ -0,0 +1,15 @@ + + * + * 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 +{ +} diff --git a/Tests/Logger/ElasticaLoggerTest.php b/Tests/Logger/ElasticaLoggerTest.php index 96adf53..7d90639 100644 --- a/Tests/Logger/ElasticaLoggerTest.php +++ b/Tests/Logger/ElasticaLoggerTest.php @@ -22,7 +22,8 @@ class ElasticaLoggerTest extends \PHPUnit_Framework_TestCase /** * @param string $level * @param string $message - * @param array $context + * @param array $context + * * @return ElasticaLogger */ private function getMockLoggerForLevelMessageAndContext($level, $message, $context) @@ -45,7 +46,7 @@ class ElasticaLoggerTest extends \PHPUnit_Framework_TestCase public function testGetZeroIfNoQueriesAdded() { - $elasticaLogger = new ElasticaLogger; + $elasticaLogger = new ElasticaLogger(); $this->assertEquals(0, $elasticaLogger->getNbQueries()); } diff --git a/Tests/Manager/RepositoryManagerTest.php b/Tests/Manager/RepositoryManagerTest.php index 8849035..71bb076 100644 --- a/Tests/Manager/RepositoryManagerTest.php +++ b/Tests/Manager/RepositoryManagerTest.php @@ -4,16 +4,19 @@ namespace FOS\ElasticaBundle\Tests\Manager; use FOS\ElasticaBundle\Manager\RepositoryManager; -class CustomRepository{} +class CustomRepository +{ +} -class Entity{} +class Entity +{ +} /** * @author Richard Miller */ class RepositoryManagerTest extends \PHPUnit_Framework_TestCase { - public function testThatGetRepositoryReturnsDefaultRepository() { /** @var $finderMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ diff --git a/Tests/Persister/ObjectPersisterTest.php b/Tests/Persister/ObjectPersisterTest.php index 497c286..06039f0 100644 --- a/Tests/Persister/ObjectPersisterTest.php +++ b/Tests/Persister/ObjectPersisterTest.php @@ -31,13 +31,6 @@ class InvalidObjectPersister extends ObjectPersister class ObjectPersisterTest extends \PHPUnit_Framework_TestCase { - public function setUp() - { - if (!class_exists('Elastica\Type')) { - $this->markTestSkipped('The Elastica library classes are not available'); - } - } - public function testThatCanReplaceObject() { $transformer = $this->getTransformer(); @@ -47,10 +40,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 +81,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 +120,7 @@ class ObjectPersisterTest extends \PHPUnit_Framework_TestCase ->disableOriginalConstructor() ->getMock(); $typeMock->expects($this->once()) - ->method('deleteById'); + ->method('deleteDocuments'); $typeMock->expects($this->never()) ->method('addDocument'); @@ -213,7 +203,7 @@ class ObjectPersisterTest extends \PHPUnit_Framework_TestCase private function getTransformer() { $transformer = new ModelToElasticaAutoTransformer(); - $transformer->setPropertyAccessor(PropertyAccess::getPropertyAccessor()); + $transformer->setPropertyAccessor(PropertyAccess::createPropertyAccessor()); return $transformer; } diff --git a/Tests/Persister/ObjectSerializerPersisterTest.php b/Tests/Persister/ObjectSerializerPersisterTest.php index aae3a64..0536d06 100644 --- a/Tests/Persister/ObjectSerializerPersisterTest.php +++ b/Tests/Persister/ObjectSerializerPersisterTest.php @@ -2,9 +2,7 @@ namespace FOS\ElasticaBundle\Tests\ObjectSerializerPersister; -use FOS\ElasticaBundle\Persister\ObjectPersister; use FOS\ElasticaBundle\Persister\ObjectSerializerPersister; -use FOS\ElasticaBundle\Transformer\ModelToElasticaAutoTransformer; use FOS\ElasticaBundle\Transformer\ModelToElasticaIdentifierTransformer; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -26,13 +24,6 @@ class POPO class ObjectSerializerPersisterTest extends \PHPUnit_Framework_TestCase { - public function setUp() - { - if (!class_exists('Elastica\Type')) { - $this->markTestSkipped('The Elastica library classes are not available'); - } - } - public function testThatCanReplaceObject() { $transformer = $this->getTransformer(); @@ -42,10 +33,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 +53,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 +71,7 @@ class ObjectSerializerPersisterTest extends \PHPUnit_Framework_TestCase ->disableOriginalConstructor() ->getMock(); $typeMock->expects($this->once()) - ->method('deleteById'); + ->method('deleteDocuments'); $typeMock->expects($this->never()) ->method('addDocument'); @@ -124,7 +112,7 @@ class ObjectSerializerPersisterTest extends \PHPUnit_Framework_TestCase private function getTransformer() { $transformer = new ModelToElasticaIdentifierTransformer(); - $transformer->setPropertyAccessor(PropertyAccess::getPropertyAccessor()); + $transformer->setPropertyAccessor(PropertyAccess::createPropertyAccessor()); return $transformer; } diff --git a/Tests/Provider/IndexableTest.php b/Tests/Provider/IndexableTest.php new file mode 100644 index 0000000..e122ec1 --- /dev/null +++ b/Tests/Provider/IndexableTest.php @@ -0,0 +1,123 @@ + + * + * 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('@indexableService', 'internalMethod')), + 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(array('@indexableService'), true), + array(function (Entity $entity) { return $entity->maybeIndex(); }, true), + array('entity.maybeIndex()', true), + array('!object.isIndexable() && entity.property == "abc"', true), + array('entity.property != "abc"', false), + array('["array", "values"]', true), + array('[]', false) + ); + } + + 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() + { + } + + public function __invoke($object) + { + return true; + } +} diff --git a/Tests/RepositoryTest.php b/Tests/RepositoryTest.php index c4d4efc..7702af2 100644 --- a/Tests/RepositoryTest.php +++ b/Tests/RepositoryTest.php @@ -13,14 +13,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase { $testQuery = 'Test Query'; - /** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ - $finderMock = $this->getMockBuilder('FOS\ElasticaBundle\Finder\TransformedFinder') - ->disableOriginalConstructor() - ->getMock(); - $finderMock->expects($this->once()) - ->method('find') - ->with($this->equalTo($testQuery)); - + $finderMock = $this->getFinderMock($testQuery); $repository = new Repository($finderMock); $repository->find($testQuery); } @@ -30,14 +23,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase $testQuery = 'Test Query'; $testLimit = 20; - /** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ - $finderMock = $this->getMockBuilder('FOS\ElasticaBundle\Finder\TransformedFinder') - ->disableOriginalConstructor() - ->getMock(); - $finderMock->expects($this->once()) - ->method('find') - ->with($this->equalTo($testQuery), $this->equalTo($testLimit)); - + $finderMock = $this->getFinderMock($testQuery, $testLimit); $repository = new Repository($finderMock); $repository->find($testQuery, $testLimit); } @@ -46,14 +32,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase { $testQuery = 'Test Query'; - /** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ - $finderMock = $this->getMockBuilder('FOS\ElasticaBundle\Finder\TransformedFinder') - ->disableOriginalConstructor() - ->getMock(); - $finderMock->expects($this->once()) - ->method('findPaginated') - ->with($this->equalTo($testQuery)); - + $finderMock = $this->getFinderMock($testQuery, array(), 'findPaginated'); $repository = new Repository($finderMock); $repository->findPaginated($testQuery); } @@ -62,14 +41,7 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase { $testQuery = 'Test Query'; - /** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ - $finderMock = $this->getMockBuilder('FOS\ElasticaBundle\Finder\TransformedFinder') - ->disableOriginalConstructor() - ->getMock(); - $finderMock->expects($this->once()) - ->method('createPaginatorAdapter') - ->with($this->equalTo($testQuery)); - + $finderMock = $this->getFinderMock($testQuery, array(), 'createPaginatorAdapter'); $repository = new Repository($finderMock); $repository->createPaginatorAdapter($testQuery); } @@ -77,17 +49,28 @@ class RepositoryTest extends \PHPUnit_Framework_TestCase public function testThatFindHybridCallsFindHybridOnFinder() { $testQuery = 'Test Query'; - $testLimit = 20; - /** @var $typeMock \PHPUnit_Framework_MockObject_MockObject|\FOS\ElasticaBundle\Finder\TransformedFinder */ + $finderMock = $this->getFinderMock($testQuery, null, 'findHybrid'); + $repository = new Repository($finderMock); + $repository->findHybrid($testQuery); + } + + /** + * @param string $testQuery + * @param mixed $testLimit + * @param string $method + * + * @return \FOS\ElasticaBundle\Finder\TransformedFinder + */ + private function getFinderMock($testQuery, $testLimit = null, $method = 'find') + { $finderMock = $this->getMockBuilder('FOS\ElasticaBundle\Finder\TransformedFinder') ->disableOriginalConstructor() ->getMock(); $finderMock->expects($this->once()) - ->method('findHybrid') + ->method($method) ->with($this->equalTo($testQuery), $this->equalTo($testLimit)); - $repository = new Repository($finderMock); - $repository->findHybrid($testQuery, $testLimit); + return $finderMock; } } diff --git a/Tests/ResetterTest.php b/Tests/ResetterTest.php deleted file mode 100644 index b4e5649..0000000 --- a/Tests/ResetterTest.php +++ /dev/null @@ -1,203 +0,0 @@ -indexConfigsByName = array( - 'foo' => array( - 'index' => $this->getMockElasticaIndex(), - 'config' => array( - 'mappings' => array( - 'a' => array( - 'dynamic_templates' => array(), - 'properties' => array(), - ), - 'b' => array('properties' => array()), - ), - ), - ), - 'bar' => array( - 'index' => $this->getMockElasticaIndex(), - 'config' => array( - 'mappings' => array( - 'a' => array('properties' => array()), - 'b' => array('properties' => array()), - ), - ), - ), - 'parent' => array( - 'index' => $this->getMockElasticaIndex(), - 'config' => array( - 'mappings' => array( - 'a' => array( - 'properties' => array( - 'field_2' => array() - ), - '_parent' => array( - 'type' => 'b', - 'property' => 'b', - 'identifier' => 'id' - ), - ), - 'b' => array('properties' => array()), - ), - ), - ), - ); - } - - public function testResetAllIndexes() - { - $this->indexConfigsByName['foo']['index']->expects($this->once()) - ->method('create') - ->with($this->indexConfigsByName['foo']['config'], true); - - $this->indexConfigsByName['bar']['index']->expects($this->once()) - ->method('create') - ->with($this->indexConfigsByName['bar']['config'], true); - - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetAllIndexes(); - } - - public function testResetIndex() - { - $this->indexConfigsByName['foo']['index']->expects($this->once()) - ->method('create') - ->with($this->indexConfigsByName['foo']['config'], true); - - $this->indexConfigsByName['bar']['index']->expects($this->never()) - ->method('create'); - - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndex('foo'); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testResetIndexShouldThrowExceptionForInvalidIndex() - { - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndex('baz'); - } - - public function testResetIndexType() - { - $type = $this->getMockElasticaType(); - - $this->indexConfigsByName['foo']['index']->expects($this->once()) - ->method('getType') - ->with('a') - ->will($this->returnValue($type)); - - $type->expects($this->once()) - ->method('delete'); - - $mapping = Mapping::create($this->indexConfigsByName['foo']['config']['mappings']['a']['properties']); - $mapping->setParam('dynamic_templates', $this->indexConfigsByName['foo']['config']['mappings']['a']['dynamic_templates']); - $type->expects($this->once()) - ->method('setMapping') - ->with($mapping); - - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndexType('foo', 'a'); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testResetIndexTypeShouldThrowExceptionForInvalidIndex() - { - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndexType('baz', 'a'); - } - - /** - * @expectedException \InvalidArgumentException - */ - public function testResetIndexTypeShouldThrowExceptionForInvalidType() - { - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndexType('foo', 'c'); - } - - public function testResetIndexTypeIgnoreTypeMissingException() - { - $type = $this->getMockElasticaType(); - - $this->indexConfigsByName['foo']['index']->expects($this->once()) - ->method('getType') - ->with('a') - ->will($this->returnValue($type)); - - $type->expects($this->once()) - ->method('delete') - ->will($this->throwException(new ResponseException( - new Request(''), - new Response(array('error' => 'TypeMissingException[[de_20131022] type[bla] missing]', 'status' => 404))) - )); - - $mapping = Mapping::create($this->indexConfigsByName['foo']['config']['mappings']['a']['properties']); - $mapping->setParam('dynamic_templates', $this->indexConfigsByName['foo']['config']['mappings']['a']['dynamic_templates']); - $type->expects($this->once()) - ->method('setMapping') - ->with($mapping); - - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndexType('foo', 'a'); - } - - public function testIndexMappingForParent() - { - $type = $this->getMockElasticaType(); - - $this->indexConfigsByName['parent']['index']->expects($this->once()) - ->method('getType') - ->with('a') - ->will($this->returnValue($type)); - - $type->expects($this->once()) - ->method('delete'); - - $mapping = Mapping::create($this->indexConfigsByName['parent']['config']['mappings']['a']['properties']); - $mapping->setParam('_parent', array('type' => 'b')); - $type->expects($this->once()) - ->method('setMapping') - ->with($mapping); - - $resetter = new Resetter($this->indexConfigsByName); - $resetter->resetIndexType('parent', 'a'); - } - - /** - * @return \Elastica\Index - */ - private function getMockElasticaIndex() - { - return $this->getMockBuilder('Elastica\Index') - ->disableOriginalConstructor() - ->getMock(); - } - - /** - * @return \Elastica\Type - */ - private function getMockElasticaType() - { - return $this->getMockBuilder('Elastica\Type') - ->disableOriginalConstructor() - ->getMock(); - } -} diff --git a/Tests/Transformer/ElasticaToModelTransformerCollectionTest.php b/Tests/Transformer/ElasticaToModelTransformerCollectionTest.php index eb4d8e4..56a7200 100644 --- a/Tests/Transformer/ElasticaToModelTransformerCollectionTest.php +++ b/Tests/Transformer/ElasticaToModelTransformerCollectionTest.php @@ -37,7 +37,7 @@ class ElasticaToModelTransformerCollectionTest extends \PHPUnit_Framework_TestCa $this->collection = new ElasticaToModelTransformerCollection($this->transformers = array( 'type1' => $transformer1, 'type2' => $transformer2, - ), array()); + )); } public function testGetObjectClass() @@ -47,7 +47,7 @@ class ElasticaToModelTransformerCollectionTest extends \PHPUnit_Framework_TestCa $objectClasses = $this->collection->getObjectClass(); $this->assertEquals(array( 'type1' => 'FOS\ElasticaBundle\Tests\Transformer\POPO', - 'type2' => 'FOS\ElasticaBundle\Tests\Transformer\POPO2' + 'type2' => 'FOS\ElasticaBundle\Tests\Transformer\POPO2', ), $objectClasses); } @@ -89,8 +89,8 @@ class ElasticaToModelTransformerCollectionTest extends \PHPUnit_Framework_TestCa $this->transformers['type1']->expects($this->once()) ->method('transform') - ->with(array($document1,$document2)) - ->will($this->returnValue(array($result1,$result2))); + ->with(array($document1, $document2)) + ->will($this->returnValue(array($result1, $result2))); $results = $this->collection->transform(array($document1, $document2)); @@ -120,8 +120,8 @@ class ElasticaToModelTransformerCollectionTest extends \PHPUnit_Framework_TestCa return array( array( - $result, $transformedObject - ) + $result, $transformedObject, + ), ); } @@ -157,6 +157,9 @@ class POPO public $id; public $data; + /** + * @param integer $id + */ public function __construct($id, $data) { $this->data = $data; diff --git a/Tests/Transformer/ModelToElasticaAutoTransformerTest.php b/Tests/Transformer/ModelToElasticaAutoTransformerTest.php index 1fa6a8e..f45134e 100644 --- a/Tests/Transformer/ModelToElasticaAutoTransformerTest.php +++ b/Tests/Transformer/ModelToElasticaAutoTransformerTest.php @@ -2,6 +2,7 @@ namespace FOS\ElasticaBundle\Tests\Transformer\ModelToElasticaAutoTransformer; +use FOS\ElasticaBundle\Event\TransformEvent; use FOS\ElasticaBundle\Transformer\ModelToElasticaAutoTransformer; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -21,8 +22,8 @@ class POPO public function __construct() { $this->date = new \DateTime('1979-05-05'); - $this->file = new \SplFileInfo(__DIR__ . '/../fixtures/attachment.odt'); - $this->fileContents = file_get_contents(__DIR__ . '/../fixtures/attachment.odt'); + $this->file = new \SplFileInfo(__DIR__.'/../fixtures/attachment.odt'); + $this->fileContents = file_get_contents(__DIR__.'/../fixtures/attachment.odt'); } public function getId() @@ -47,7 +48,7 @@ class POPO { return array( 'key1' => 'value1', - 'key2' => 'value2' + 'key2' => 'value2', ); } @@ -109,7 +110,7 @@ class POPO public function getNestedObject() { - return array('key1' => (object)array('id' => 1, 'key1sub1' => 'value1sub1', 'key1sub2' => 'value1sub2')); + return array('key1' => (object) array('id' => 1, 'key1sub1' => 'value1sub1', 'key1sub2' => 'value1sub2')); } public function getUpper() @@ -125,11 +126,33 @@ class POPO class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase { - public function setUp() + public function testTransformerDispatches() { - if (!class_exists('Elastica\Document')) { - $this->markTestSkipped('The Elastica library classes are not available'); - } + $dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface') + ->getMock(); + $dispatcher->expects($this->once()) + ->method('dispatch') + ->with( + TransformEvent::POST_TRANSFORM, + $this->isInstanceOf('FOS\ElasticaBundle\Event\TransformEvent') + ); + + $transformer = $this->getTransformer($dispatcher); + $transformer->transform(new POPO(), array()); + } + + public function testPropertyPath() + { + $transformer = $this->getTransformer(); + + $document = $transformer->transform(new POPO(), array('name' => array('property_path' => false))); + $this->assertInstanceOf('Elastica\Document', $document); + $this->assertFalse($document->has('name')); + + $document = $transformer->transform(new POPO(), array('realName' => array('property_path' => 'name'))); + $this->assertInstanceOf('Elastica\Document', $document); + $this->assertTrue($document->has('realName')); + $this->assertEquals('someName', $document->get('realName')); } public function testThatCanTransformObject() @@ -152,7 +175,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase 'float' => array(), 'bool' => array(), 'date' => array(), - 'falseBool' => array() + 'falseBool' => array(), ) ); $data = $document->getData(); @@ -185,7 +208,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $this->assertEquals( array( 'key1' => 'value1', - 'key2' => 'value2' + 'key2' => 'value2', ), $data['array'] ); } @@ -230,7 +253,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $document = $transformer->transform(new POPO(), array('file' => array('type' => 'attachment'))); $data = $document->getData(); - $this->assertEquals(base64_encode(file_get_contents(__DIR__ . '/../fixtures/attachment.odt')), $data['file']); + $this->assertEquals(base64_encode(file_get_contents(__DIR__.'/../fixtures/attachment.odt')), $data['file']); } public function testFileContentsAddedForAttachmentMapping() @@ -240,7 +263,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $data = $document->getData(); $this->assertEquals( - base64_encode(file_get_contents(__DIR__ . '/../fixtures/attachment.odt')), $data['fileContents'] + base64_encode(file_get_contents(__DIR__.'/../fixtures/attachment.odt')), $data['fileContents'] ); } @@ -248,18 +271,18 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase { $transformer = $this->getTransformer(); $document = $transformer->transform(new POPO(), array( - 'sub' => array( - 'type' => 'nested', - 'properties' => array('foo' => '~') - ) - )); + 'sub' => array( + 'type' => 'nested', + 'properties' => array('foo' => array()), + ), + )); $data = $document->getData(); $this->assertTrue(array_key_exists('sub', $data)); $this->assertInternalType('array', $data['sub']); $this->assertEquals(array( array('foo' => 'foo'), - array('foo' => 'bar') + array('foo' => 'bar'), ), $data['sub']); } @@ -269,8 +292,8 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $document = $transformer->transform(new POPO(), array( 'sub' => array( 'type' => 'object', - 'properties' => array('bar') - ) + 'properties' => array('bar'), + ), )); $data = $document->getData(); @@ -278,7 +301,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $this->assertInternalType('array', $data['sub']); $this->assertEquals(array( array('bar' => 'foo'), - array('bar' => 'bar') + array('bar' => 'bar'), ), $data['sub']); } @@ -287,18 +310,18 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $transformer = $this->getTransformer(); $document = $transformer->transform(new POPO(), array( 'obj' => array( - 'type' => 'object' - ) + 'type' => 'object', + ), )); $data = $document->getData(); $this->assertTrue(array_key_exists('obj', $data)); $this->assertInternalType('array', $data['obj']); $this->assertEquals(array( - 'foo' => 'foo', - 'bar' => 'foo', - 'id' => 1 - ), $data['obj']); + 'foo' => 'foo', + 'bar' => 'foo', + 'id' => 1, + ), $data['obj']); } public function testObjectsMappingOfAtLeastOneAutoMappedObjectAndAtLeastOneManuallyMappedObject() @@ -313,14 +336,14 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase 'properties' => array( 'key1sub1' => array( 'type' => 'string', - 'properties' => array() + 'properties' => array(), ), 'key1sub2' => array( 'type' => 'string', - 'properties' => array() - ) - ) - ) + 'properties' => array(), + ), + ), + ), ) ); $data = $document->getData(); @@ -333,14 +356,14 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase array( 'foo' => 'foo', 'bar' => 'foo', - 'id' => 1 + 'id' => 1, ), $data['obj'] ); $this->assertEquals( array( 'key1sub1' => 'value1sub1', - 'key1sub2' => 'value1sub2' + 'key1sub2' => 'value1sub2', ), $data['nestedObject'][0] ); @@ -350,7 +373,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase { $transformer = $this->getTransformer(); $document = $transformer->transform(new POPO(), array( - '_parent' => array('type' => 'upper', 'property'=>'upper', 'identifier' => 'id'), + '_parent' => array('type' => 'upper', 'property' => 'upper', 'identifier' => 'id'), )); $this->assertEquals("parent", $document->getParent()); @@ -360,7 +383,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase { $transformer = $this->getTransformer(); $document = $transformer->transform(new POPO(), array( - '_parent' => array('type' => 'upper', 'property'=>'upper', 'identifier' => 'name'), + '_parent' => array('type' => 'upper', 'property' => 'upper', 'identifier' => 'name'), )); $this->assertEquals("a random name", $document->getParent()); @@ -370,7 +393,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase { $transformer = $this->getTransformer(); $document = $transformer->transform(new POPO(), array( - '_parent' => array('type' => 'upper', 'property'=>null, 'identifier' => 'id'), + '_parent' => array('type' => 'upper', 'property' => null, 'identifier' => 'id'), )); $this->assertEquals("parent", $document->getParent()); @@ -380,19 +403,21 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase { $transformer = $this->getTransformer(); $document = $transformer->transform(new POPO(), array( - '_parent' => array('type' => 'upper', 'property'=>'upperAlias', 'identifier' => 'id'), + '_parent' => array('type' => 'upper', 'property' => 'upperAlias', 'identifier' => 'id'), )); $this->assertEquals("parent", $document->getParent()); } /** + * @param null|\Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher + * * @return ModelToElasticaAutoTransformer */ - private function getTransformer() + private function getTransformer($dispatcher = null) { - $transformer = new ModelToElasticaAutoTransformer(); - $transformer->setPropertyAccessor(PropertyAccess::getPropertyAccessor()); + $transformer = new ModelToElasticaAutoTransformer(array(), $dispatcher); + $transformer->setPropertyAccessor(PropertyAccess::createPropertyAccessor()); return $transformer; } diff --git a/Tests/Transformer/ModelToElasticaIdentifierTransformerTest.php b/Tests/Transformer/ModelToElasticaIdentifierTransformerTest.php index f1a77d4..aa3d7b7 100644 --- a/Tests/Transformer/ModelToElasticaIdentifierTransformerTest.php +++ b/Tests/Transformer/ModelToElasticaIdentifierTransformerTest.php @@ -23,13 +23,6 @@ class POPO class ModelToElasticaIdentifierTransformerTest extends \PHPUnit_Framework_TestCase { - public function setUp() - { - if (!class_exists('Elastica\Document')) { - $this->markTestSkipped('The Elastica library classes are not available'); - } - } - public function testGetDocumentWithIdentifierOnly() { $transformer = $this->getTransformer(); @@ -58,7 +51,7 @@ class ModelToElasticaIdentifierTransformerTest extends \PHPUnit_Framework_TestCa private function getTransformer() { $transformer = new ModelToElasticaIdentifierTransformer(); - $transformer->setPropertyAccessor(PropertyAccess::getPropertyAccessor()); + $transformer->setPropertyAccessor(PropertyAccess::createPropertyAccessor()); return $transformer; } diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php deleted file mode 100644 index 30fb165..0000000 --- a/Tests/bootstrap.php +++ /dev/null @@ -1,19 +0,0 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\ElasticaBundle\Transformer; + +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +abstract class AbstractElasticaToModelTransformer implements ElasticaToModelTransformerInterface +{ + /** + * PropertyAccessor instance. + * + * @var PropertyAccessorInterface + */ + protected $propertyAccessor; + + /** + * Set the PropertyAccessor instance. + * + * @param PropertyAccessorInterface $propertyAccessor + */ + public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor) + { + $this->propertyAccessor = $propertyAccessor; + } + + /** + * Returns a sorting closure to be used with usort() to put retrieved objects + * back in the order that they were returned by ElasticSearch. + * + * @param array $idPos + * @param string $identifierPath + * @return callable + */ + protected function getSortingClosure(array $idPos, $identifierPath) + { + $propertyAccessor = $this->propertyAccessor; + + return function ($a, $b) use ($idPos, $identifierPath, $propertyAccessor) { + return $idPos[$propertyAccessor->getValue($a, $identifierPath)] > $idPos[$propertyAccessor->getValue($b, $identifierPath)]; + }; + } +} diff --git a/Transformer/ElasticaToModelTransformerCollection.php b/Transformer/ElasticaToModelTransformerCollection.php index f65f8db..9920f43 100644 --- a/Transformer/ElasticaToModelTransformerCollection.php +++ b/Transformer/ElasticaToModelTransformerCollection.php @@ -41,6 +41,7 @@ class ElasticaToModelTransformerCollection implements ElasticaToModelTransformer /** * @param Document[] $elasticaObjects + * * @return array */ public function transform(array $elasticaObjects) @@ -51,12 +52,12 @@ class ElasticaToModelTransformerCollection implements ElasticaToModelTransformer } $transformed = array(); - foreach ($sorted AS $type => $objects) { + foreach ($sorted as $type => $objects) { $transformedObjects = $this->transformers[$type]->transform($objects); - $identifierGetter = 'get' . ucfirst($this->transformers[$type]->getIdentifierField()); + $identifierGetter = 'get'.ucfirst($this->transformers[$type]->getIdentifierField()); $transformed[$type] = array_combine( array_map( - function($o) use ($identifierGetter) { + function ($o) use ($identifierGetter) { return $o->$identifierGetter(); }, $transformedObjects @@ -80,7 +81,7 @@ class ElasticaToModelTransformerCollection implements ElasticaToModelTransformer $objects = $this->transform($elasticaObjects); $result = array(); - for ($i = 0; $i < count($elasticaObjects); $i++) { + for ($i = 0, $j = count($elasticaObjects); $i < $j; $i++) { $result[] = new HybridResult($elasticaObjects[$i], $objects[$i]); } diff --git a/Transformer/ElasticaToModelTransformerInterface.php b/Transformer/ElasticaToModelTransformerInterface.php index 5635ef3..71cd651 100644 --- a/Transformer/ElasticaToModelTransformerInterface.php +++ b/Transformer/ElasticaToModelTransformerInterface.php @@ -3,32 +3,33 @@ namespace FOS\ElasticaBundle\Transformer; /** - * Maps Elastica documents with model objects + * Maps Elastica documents with model objects. */ interface ElasticaToModelTransformerInterface { /** * Transforms an array of elastica objects into an array of - * model objects fetched from the doctrine repository + * model objects fetched from the doctrine repository. * * @param array $elasticaObjects array of elastica objects + * * @return array of model objects **/ - function transform(array $elasticaObjects); + public function transform(array $elasticaObjects); - function hybridTransform(array $elasticaObjects); + public function hybridTransform(array $elasticaObjects); /** * Returns the object class used by the transformer. * * @return string */ - function getObjectClass(); + public function getObjectClass(); /** - * Returns the identifier field from the options + * Returns the identifier field from the options. * * @return string the identifier field */ - function getIdentifierField(); + public function getIdentifierField(); } diff --git a/Transformer/HighlightableModelInterface.php b/Transformer/HighlightableModelInterface.php index d55407e..96c6c7c 100644 --- a/Transformer/HighlightableModelInterface.php +++ b/Transformer/HighlightableModelInterface.php @@ -1,16 +1,23 @@ - 'id' + 'identifier' => 'id', ); /** - * PropertyAccessor instance + * PropertyAccessor instance. * * @var PropertyAccessorInterface */ protected $propertyAccessor; /** - * Instanciates a new Mapper + * Instanciates a new Mapper. * - * @param array $options + * @param array $options + * @param EventDispatcherInterface $dispatcher */ - public function __construct(array $options = array()) + public function __construct(array $options = array(), EventDispatcherInterface $dispatcher = null) { $this->options = array_merge($this->options, $options); + $this->dispatcher = $dispatcher; } /** - * Set the PropertyAccessor + * Set the PropertyAccessor. * * @param PropertyAccessorInterface $propertyAccessor */ @@ -49,7 +58,7 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf } /** - * Transforms an object into an elastica object having the required keys + * Transforms an object into an elastica object having the required keys. * * @param object $object the object to convert * @param array $fields the keys we want to have in the returned array @@ -63,19 +72,27 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf foreach ($fields as $key => $mapping) { if ($key == '_parent') { - $property = (null !== $mapping['property'])?$mapping['property']:$mapping['type']; + $property = (null !== $mapping['property']) ? $mapping['property'] : $mapping['type']; $value = $this->propertyAccessor->getValue($object, $property); $document->setParent($this->propertyAccessor->getValue($value, $mapping['identifier'])); + continue; } - $value = $this->propertyAccessor->getValue($object, $key); + $path = isset($mapping['property_path']) ? + $mapping['property_path'] : + $key; + if (false === $path) { + continue; + } + $value = $this->propertyAccessor->getValue($object, $path); if (isset($mapping['type']) && in_array($mapping['type'], array('nested', 'object')) && isset($mapping['properties']) && !empty($mapping['properties'])) { /* $value is a nested document or object. Transform $value into * an array of documents, respective the mapped properties. */ $document->set($key, $this->transformNested($value, $mapping['properties'])); + continue; } @@ -86,20 +103,28 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf } else { $document->addFileContent($key, $value); } + continue; } $document->set($key, $this->normalizeValue($value)); } + if ($this->dispatcher) { + $event = new TransformEvent($document, $fields, $object); + $this->dispatcher->dispatch(TransformEvent::POST_TRANSFORM, $event); + + $document = $event->getDocument(); + } + return $document; } /** - * transform a nested document or an object property into an array of ElasticaDocument + * transform a nested document or an object property into an array of ElasticaDocument. * * @param array|\Traversable|\ArrayAccess $objects the object to convert - * @param array $fields the keys we want to have in the returned array + * @param array $fields the keys we want to have in the returned array * * @return array */ @@ -123,7 +148,7 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf } /** - * Attempts to convert any type to a string or an array of strings + * Attempts to convert any type to a string or an array of strings. * * @param mixed $value * @@ -131,17 +156,16 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf */ protected function normalizeValue($value) { - $normalizeValue = function(&$v) - { + $normalizeValue = function (&$v) { if ($v instanceof \DateTime) { $v = $v->format('c'); } elseif (!is_scalar($v) && !is_null($v)) { - $v = (string)$v; + $v = (string) $v; } }; 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..6301be1 100644 --- a/Transformer/ModelToElasticaIdentifierTransformer.php +++ b/Transformer/ModelToElasticaIdentifierTransformer.php @@ -6,12 +6,12 @@ use Elastica\Document; /** * Creates an Elastica document with the ID of - * the Doctrine object as Elastica document ID + * the Doctrine object as Elastica document ID. */ class ModelToElasticaIdentifierTransformer extends ModelToElasticaAutoTransformer { /** - * Creates an elastica document with the id of the doctrine object as id + * Creates an elastica document with the id of the doctrine object as id. * * @param object $object the object to convert * @param array $fields the keys we want to have in the returned array @@ -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/Transformer/ModelToElasticaTransformerInterface.php b/Transformer/ModelToElasticaTransformerInterface.php index ec9ada3..0ad9f12 100644 --- a/Transformer/ModelToElasticaTransformerInterface.php +++ b/Transformer/ModelToElasticaTransformerInterface.php @@ -3,16 +3,17 @@ namespace FOS\ElasticaBundle\Transformer; /** - * Maps Elastica documents with model objects + * Maps Elastica documents with model objects. */ interface ModelToElasticaTransformerInterface { /** - * Transforms an object into an elastica object having the required keys + * Transforms an object into an elastica object having the required keys. * * @param object $object the object to convert - * @param array $fields the keys we want to have in the returned array + * @param array $fields the keys we want to have in the returned array + * * @return \Elastica\Document **/ - function transform($object, array $fields); + public function transform($object, array $fields); } diff --git a/composer.json b/composer.json index a991d38..9705a04 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,28 @@ "symfony/console": "~2.1", "symfony/form": "~2.1", "symfony/property-access": "~2.2", - "ruflin/elastica": ">=0.90.10.0, <1.2-dev", + "ruflin/elastica": ">=0.90.10.0, <1.5-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", + "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" - }, - "suggest": { - "doctrine/orm": ">=2.2,<2.5-dev", - "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" + "pagerfanta/pagerfanta": "~1.0", + "knplabs/knp-components": "~1.2", + "knplabs/knp-paginator-bundle": "~2.4", + "symfony/browser-kit" : "~2.3", + "symfony/expression-language" : "~2.4", + "symfony/twig-bundle": "~2.3" }, "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.2.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