diff --git a/.travis.yml b/.travis.yml index a83f9b9..fbb22d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ language: php +cache: + directories: + - $HOME/.composer/cache + php: - 5.3 - 5.4 @@ -21,7 +25,8 @@ before_script: - sh -c 'if [ "$SYMFONY_VERSION" != "" ]; then composer require --dev --no-update symfony/symfony=$SYMFONY_VERSION; fi;' - composer install --dev --prefer-source -script: vendor/bin/phpunit --coverage-clover=coverage.clover +script: + - vendor/bin/phpunit --coverage-clover=coverage.clover services: - elasticsearch diff --git a/CHANGELOG-3.0.md b/CHANGELOG-3.0.md index 2596a29..973269d 100644 --- a/CHANGELOG-3.0.md +++ b/CHANGELOG-3.0.md @@ -12,6 +12,24 @@ https://github.com/FriendsOfSymfony/FOSElasticaBundle/compare/v3.0.0...v3.0.1 To generate a changelog summary since the last version, run `git log --no-merges --oneline v3.0.0...3.0.x` +* 3.0.8 (2014-01-31) + + * Fixed handling of empty indexes #760 + * Added support for `connectionStrategy` Elastica configuration #732 + * Allow Elastica 1.4 + +* 3.0.7 (2015-01-21) + + * Fixed the indexing of parent/child relations, broken since 3.0 #774 + * Fixed multi_field properties not being normalised #769 + +* 3.0.6 (2015-01-04) + + * Removed unused public image asset for the web development toolbar #742 + * Fixed is_indexable_callback BC code to support array notation #761 + * Fixed debug_logger for type providers #724 + * Clean the OM if we filter away the entire batch #737 + * 3.0.0-ALPHA6 * Moved `is_indexable_callback` from the listener properties to a type property called diff --git a/CHANGELOG-3.1.md b/CHANGELOG-3.1.md index 0ce44ad..19bec1f 100644 --- a/CHANGELOG-3.1.md +++ b/CHANGELOG-3.1.md @@ -14,6 +14,20 @@ https://github.com/FriendsOfSymfony/FOSElasticaBundle/compare/v3.0.4...v3.1.0 * 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. - * Removed `Doctrine\Listener#getSubscribedEvents`. The container + * 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. + this class on every request where doctrine is active. #729 + * 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 diff --git a/Command/PopulateCommand.php b/Command/PopulateCommand.php index 89d2ebc..8b6c0f2 100644 --- a/Command/PopulateCommand.php +++ b/Command/PopulateCommand.php @@ -2,22 +2,30 @@ namespace FOS\ElasticaBundle\Command; +use FOS\ElasticaBundle\Event\IndexPopulateEvent; use FOS\ElasticaBundle\Event\PopulateEvent; +use FOS\ElasticaBundle\Event\TypePopulateEvent; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Helper\DialogHelper; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use FOS\ElasticaBundle\Index\IndexManager; +use FOS\ElasticaBundle\IndexManager; use FOS\ElasticaBundle\Provider\ProviderRegistry; +use FOS\ElasticaBundle\Resetter; use FOS\ElasticaBundle\Provider\ProviderInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Console\Helper\ProgressBar; /** * Populate the search index */ class PopulateCommand extends ContainerAwareCommand { + /** + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + private $dispatcher; + /** * @var IndexManager */ @@ -29,9 +37,9 @@ class PopulateCommand extends ContainerAwareCommand private $providerRegistry; /** - * @var EventDispatcherInterface + * @var Resetter */ - private $eventDispatcher; + private $resetter; /** * @see Symfony\Component\Console\Command\Command::configure() @@ -56,9 +64,10 @@ class PopulateCommand extends ContainerAwareCommand */ 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->eventDispatcher = $this->getContainer()->get('event_dispatcher'); + $this->resetter = $this->getContainer()->get('fos_elastica.resetter'); } /** @@ -100,6 +109,86 @@ class PopulateCommand extends ContainerAwareCommand } } + /** + * @param ProviderInterface $provider + * @param OutputInterface $output + * @param string $index + * @param string $type + * @param array $options + */ + private function doPopulateType(ProviderInterface $provider, OutputInterface $output, $index, $type, $options) + { + $event = new TypePopulateEvent($index, $type, $options); + $this->dispatcher->dispatch(TypePopulateEvent::PRE_TYPE_POPULATE, $event); + + $loggerClosure = $this->getLoggerClosure($output, $index, $type); + $provider->populate($loggerClosure, $event->getOptions()); + + $this->dispatcher->dispatch(TypePopulateEvent::POST_TYPE_POPULATE, $event); + } + + /** + * Builds a loggerClosure to be called from inside the Provider to update the command + * line. + * + * @param OutputInterface $output + * @param string $index + * @param string $type + * @return callable + */ + private function getLoggerClosure(OutputInterface $output, $index, $type) + { + if (!class_exists('Symfony\Component\Console\Helper\ProgressBar')) { + $lastStep = null; + $current = 0; + + return function ($increment, $totalObjects) use ($output, $index, $type, &$lastStep, &$current) { + if ($current + $increment > $totalObjects) { + $increment = $totalObjects - $current; + } + + $currentTime = microtime(true); + $timeDifference = $currentTime - $lastStep; + $objectsPerSecond = $lastStep ? ($increment / $timeDifference) : $increment; + $lastStep = $currentTime; + $current += $increment; + $percent = 100 * $current / $totalObjects; + + $output->writeln(sprintf( + 'Populating %s/%s %0.1f%% (%d/%d), %d objects/s (RAM: current=%uMo peak=%uMo)', + $index, + $type, + $percent, + $current, + $totalObjects, + $objectsPerSecond, + round(memory_get_usage() / (1024 * 1024)), + round(memory_get_peak_usage() / (1024 * 1024)) + )); + }; + } + + ProgressBar::setFormatDefinition('normal', " %current%/%max% [%bar%] %percent:3s%%\n%message%"); + ProgressBar::setFormatDefinition('verbose', " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%\n%message%"); + ProgressBar::setFormatDefinition('very_verbose', " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%\n%message%"); + ProgressBar::setFormatDefinition('debug', " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%\n%message%"); + $progress = null; + + return function ($increment, $totalObjects) use (&$progress, $output, $index, $type) { + if (null === $progress) { + $progress = new ProgressBar($output, $totalObjects); + $progress->start(); + } + + $progress->setMessage(sprintf('Populating %s/%s', $index, $type)); + $progress->advance($increment); + + if ($progress->getProgressPercent() >= 1.0) { + $progress->finish(); + } + }; + } + /** * Recreates an index, populates its types, and refreshes the index. * @@ -110,13 +199,23 @@ class PopulateCommand extends ContainerAwareCommand */ private function populateIndex(OutputInterface $output, $index, $reset, $options) { - /** @var $providers ProviderInterface[] */ + $event = new IndexPopulateEvent($index, $reset, $options); + $this->dispatcher->dispatch(IndexPopulateEvent::PRE_INDEX_POPULATE, $event); + + if ($event->isReset()) { + $output->writeln(sprintf('Resetting %s', $index)); + $this->resetter->resetIndex($index, true); + } + $providers = $this->providerRegistry->getIndexProviders($index); - $this->populate($output, $providers, $index, null, $reset, $options); + foreach ($providers as $type => $provider) { + $this->doPopulateType($provider, $output, $index, $type, $event->getOptions()); + } - $output->writeln(sprintf('Refreshing %s', $index)); - $this->indexManager->getIndex($index)->refresh(); + $this->dispatcher->dispatch(IndexPopulateEvent::POST_INDEX_POPULATE, $event); + + $this->refreshIndex($output, $index); } /** @@ -130,48 +229,31 @@ class PopulateCommand extends ContainerAwareCommand */ private function populateIndexType(OutputInterface $output, $index, $type, $reset, $options) { - $provider = $this->providerRegistry->getProvider($index, $type); + if ($reset) { + $output->writeln(sprintf('Resetting %s/%s', $index, $type)); + $this->resetter->resetIndexType($index, $type); + } - $this->populate($output, array($type => $provider), $index, $type, $reset, $options); + $provider = $this->providerRegistry->getProvider($index, $type); + $this->doPopulateType($provider, $output, $index, $type, $options); + + $this->refreshIndex($output, $index, false); + } + + /** + * Refreshes an index. + * + * @param OutputInterface $output + * @param string $index + * @param bool $postPopulate + */ + private function refreshIndex(OutputInterface $output, $index, $postPopulate = true) + { + if ($postPopulate) { + $this->resetter->postPopulate($index); + } $output->writeln(sprintf('Refreshing %s', $index)); $this->indexManager->getIndex($index)->refresh(); } - - /** - * @param OutputInterface $output - * @param ProviderInterface[] $providers - * @param string $index - * @param string $type - * @param boolean $reset - * @param array $options - */ - private function populate(OutputInterface $output, array $providers, $index, $type, $reset, $options) - { - if ($reset) { - if ($type) { - $output->writeln(sprintf('Resetting %s/%s', $index, $type)); - } else { - $output->writeln(sprintf('Resetting %s', $index)); - } - } - - $this->eventDispatcher->dispatch(PopulateEvent::PRE_INDEX_POPULATE, new PopulateEvent($index, $type, $reset, $options)); - - foreach ($providers as $providerType => $provider) { - $event = new PopulateEvent($index, $providerType, $reset, $options); - - $this->eventDispatcher->dispatch(PopulateEvent::PRE_TYPE_POPULATE, $event); - - $loggerClosure = function($message) use ($output, $index, $providerType) { - $output->writeln(sprintf('Populating %s/%s, %s', $index, $providerType, $message)); - }; - - $provider->populate($loggerClosure, $options); - - $this->eventDispatcher->dispatch(PopulateEvent::POST_TYPE_POPULATE, $event); - } - - $this->eventDispatcher->dispatch(PopulateEvent::POST_INDEX_POPULATE, new PopulateEvent($index, $type, $reset, $options)); - } } diff --git a/Configuration/Source/ContainerSource.php b/Configuration/Source/ContainerSource.php index 8d094c7..abcdf1b 100644 --- a/Configuration/Source/ContainerSource.php +++ b/Configuration/Source/ContainerSource.php @@ -40,16 +40,7 @@ class ContainerSource implements SourceInterface { $indexes = array(); foreach ($this->configArray as $config) { - $types = array(); - foreach ($config['types'] as $typeConfig) { - $types[$typeConfig['name']] = new TypeConfig( - $typeConfig['name'], - $typeConfig['mapping'], - $typeConfig['config'] - ); - // TODO: handle prototypes.. - } - + $types = $this->getTypes($config); $index = new IndexConfig($config['name'], $types, array( 'elasticSearchName' => $config['elasticsearch_name'], 'settings' => $config['settings'], @@ -61,4 +52,28 @@ class ContainerSource implements SourceInterface return $indexes; } + + /** + * Builds TypeConfig objects for each type. + * + * @param array $config + * @return array + */ + protected function getTypes($config) + { + $types = array(); + + if (isset($config['types'])) { + foreach ($config['types'] as $typeConfig) { + $types[$typeConfig['name']] = new TypeConfig( + $typeConfig['name'], + $typeConfig['mapping'], + $typeConfig['config'] + ); + // TODO: handle prototypes.. + } + } + + return $types; + } } diff --git a/Configuration/TypeConfig.php b/Configuration/TypeConfig.php index fc9041d..a46cd34 100644 --- a/Configuration/TypeConfig.php +++ b/Configuration/TypeConfig.php @@ -35,6 +35,22 @@ class TypeConfig $this->name = $name; } + /** + * @return bool|null + */ + public function getDateDetection() + { + return $this->getConfig('date_detection'); + } + + /** + * @return array + */ + public function getDynamicDateFormats() + { + return $this->getConfig('dynamic_date_formats'); + } + /** * @return string|null */ @@ -61,6 +77,14 @@ class TypeConfig null; } + /** + * @return bool|null + */ + public function getNumericDetection() + { + return $this->getConfig('numeric_detection'); + } + /** * @return string */ diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index b204f0c..3b3844e 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -84,6 +84,16 @@ class Configuration implements ConfigurationInterface return $v; }) ->end() + // Elastica names its properties with camel case, support both + ->beforeNormalization() + ->ifTrue(function ($v) { return isset($v['connection_strategy']); }) + ->then(function ($v) { + $v['connectionStrategy'] = $v['connection_strategy']; + unset($v['connection_strategy']); + + return $v; + }) + ->end() // If there is no connections array key defined, assume a single connection. ->beforeNormalization() ->ifTrue(function ($v) { return is_array($v) && !array_key_exists('connections', $v); }) @@ -124,6 +134,7 @@ class Configuration implements ConfigurationInterface ->end() ->scalarNode('timeout')->end() ->scalarNode('headers')->end() + ->scalarNode('connectionStrategy')->defaultValue('Simple')->end() ->end() ->end() ->end() @@ -199,7 +210,17 @@ class Configuration implements ConfigurationInterface isset($v['persistence']['listener']['is_indexable_callback']); }) ->then(function ($v) { - $v['indexable_callback'] = $v['persistence']['listener']['is_indexable_callback']; + $callback = $v['persistence']['listener']['is_indexable_callback']; + + if (is_array($callback)) { + list($class) = $callback + array(null); + + if ($class[0] !== '@' && is_string($class) && !class_exists($class)) { + $callback[0] = '@'.$class; + } + } + + $v['indexable_callback'] = $callback; unset($v['persistence']['listener']['is_indexable_callback']); return $v; @@ -225,7 +246,10 @@ class Configuration implements ConfigurationInterface }) ->end() ->children() + ->booleanNode('date_detection')->end() + ->arrayNode('dynamic_date_formats')->prototype('scalar')->end()->end() ->scalarNode('index_analyzer')->end() + ->booleanNode('numeric_detection')->end() ->scalarNode('search_analyzer')->end() ->variableNode('indexable_callback')->end() ->append($this->getPersistenceNode()) diff --git a/DependencyInjection/FOSElasticaExtension.php b/DependencyInjection/FOSElasticaExtension.php index 705da12..1e446f7 100644 --- a/DependencyInjection/FOSElasticaExtension.php +++ b/DependencyInjection/FOSElasticaExtension.php @@ -241,6 +241,9 @@ class FOSElasticaExtension extends Extension 'serializer', 'index_analyzer', 'search_analyzer', + 'date_detection', + 'dynamic_date_formats', + 'numeric_detection', ) as $field) { $typeConfig['config'][$field] = array_key_exists($field, $type) ? $type[$field] : @@ -393,7 +396,12 @@ class FOSElasticaExtension extends Extension $arguments[] = array(new Reference($callbackId), 'serialize'); } else { $abstractId = 'fos_elastica.object_persister'; - $arguments[] = $this->indexConfigs[$indexName]['types'][$typeName]['mapping']['properties']; + $mapping = $this->indexConfigs[$indexName]['types'][$typeName]['mapping']; + $argument = $mapping['properties']; + if(isset($mapping['_parent'])){ + $argument['_parent'] = $mapping['_parent']; + } + $arguments[] = $argument; } $serviceId = sprintf('fos_elastica.object_persister.%s.%s', $indexName, $typeName); diff --git a/Doctrine/AbstractProvider.php b/Doctrine/AbstractProvider.php index 92be6ce..80d0716 100644 --- a/Doctrine/AbstractProvider.php +++ b/Doctrine/AbstractProvider.php @@ -39,7 +39,7 @@ abstract class AbstractProvider extends BaseAbstractProvider } /** - * @see FOS\ElasticaBundle\Provider\ProviderInterface::populate() + * {@inheritDoc} */ public function populate(\Closure $loggerClosure = null, array $options = array()) { @@ -56,34 +56,19 @@ abstract class AbstractProvider extends BaseAbstractProvider $manager = $this->managerRegistry->getManagerForClass($this->objectClass); for (; $offset < $nbObjects; $offset += $batchSize) { - if ($loggerClosure) { - $stepStartTime = microtime(true); - } $objects = $this->fetchSlice($queryBuilder, $batchSize, $offset); - if ($loggerClosure) { - $stepNbObjects = count($objects); - } $objects = array_filter($objects, array($this, 'isObjectIndexable')); - if (!$objects) { - if ($loggerClosure) { - $loggerClosure('Entire batch was filtered away, skipping...'); - } - if ($this->options['clear_object_manager']) { - $manager->clear(); - } - - continue; - } - - if (!$ignoreErrors) { - $this->objectPersister->insertMany($objects); - } else { - try { + if ($objects) { + if (!$ignoreErrors) { $this->objectPersister->insertMany($objects); - } catch(BulkResponseException $e) { - if ($loggerClosure) { - $loggerClosure(sprintf('%s',$e->getMessage())); + } else { + try { + $this->objectPersister->insertMany($objects); + } catch(BulkResponseException $e) { + if ($loggerClosure) { + $loggerClosure(sprintf('%s',$e->getMessage())); + } } } } @@ -95,11 +80,7 @@ abstract class AbstractProvider extends BaseAbstractProvider usleep($sleep); if ($loggerClosure) { - $stepCount = $stepNbObjects + $offset; - $percentComplete = 100 * $stepCount / $nbObjects; - $timeDifference = microtime(true) - $stepStartTime; - $objectsPerSecond = $timeDifference ? ($stepNbObjects / $timeDifference) : $stepNbObjects; - $loggerClosure(sprintf('%0.1f%% (%d/%d), %d objects/s %s', $percentComplete, $stepCount, $nbObjects, $objectsPerSecond, $this->getMemoryUsage())); + $loggerClosure($batchSize, $nbObjects); } } diff --git a/Event/PopulateEvent.php b/Event/IndexPopulateEvent.php similarity index 74% rename from Event/PopulateEvent.php rename to Event/IndexPopulateEvent.php index 305c393..5074f05 100644 --- a/Event/PopulateEvent.php +++ b/Event/IndexPopulateEvent.php @@ -17,24 +17,16 @@ use Symfony\Component\EventDispatcher\Event; * * @author Oleg Andreyev */ -class PopulateEvent extends Event +class IndexPopulateEvent extends Event { const PRE_INDEX_POPULATE = 'elastica.index.index_pre_populate'; const POST_INDEX_POPULATE = 'elastica.index.index_post_populate'; - const PRE_TYPE_POPULATE = 'elastica.index.type_pre_populate'; - const POST_TYPE_POPULATE = 'elastica.index.type_post_populate'; - /** * @var string */ private $index; - /** - * @var string - */ - private $type; - /** * @var bool */ @@ -47,14 +39,12 @@ class PopulateEvent extends Event /** * @param string $index - * @param string|null $type * @param boolean $reset * @param array $options */ - public function __construct($index, $type, $reset, $options) + public function __construct($index, $reset, $options) { $this->index = $index; - $this->type = $type; $this->reset = $reset; $this->options = $options; } @@ -67,14 +57,6 @@ class PopulateEvent extends Event return $this->index; } - /** - * @return string - */ - public function getType() - { - return $this->type; - } - /** * @return boolean */ 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..9239c15 --- /dev/null +++ b/Event/TypePopulateEvent.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\Event; + +use Symfony\Component\EventDispatcher\Event; + +/** + * Populate Event + * + * @author Oleg Andreyev + */ +class TypePopulateEvent extends Event +{ + const PRE_TYPE_POPULATE = 'elastica.index.type_pre_populate'; + const POST_TYPE_POPULATE = 'elastica.index.type_post_populate'; + + /** + * @var string + */ + private $index; + + /** + * @var string + */ + private $type; + + /** + * @var array + */ + private $options; + + /** + * @param string $index + * @param string $type + * @param array $options + */ + public function __construct($index, $type, $options) + { + $this->index = $index; + $this->type = $type; + $this->options = $options; + } + + /** + * @return string + */ + public function getIndex() + { + return $this->index; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } + + public function setOptions($options) + { + $this->options = $options; + } +} diff --git a/Index/MappingBuilder.php b/Index/MappingBuilder.php index 21ae871..92beaf7 100644 --- a/Index/MappingBuilder.php +++ b/Index/MappingBuilder.php @@ -58,13 +58,19 @@ class MappingBuilder */ public function buildTypeMapping(TypeConfig $typeConfig) { - $mapping = array_merge($typeConfig->getMapping(), array( - // 'date_detection' => true, - // 'dynamic_date_formats' => array() - // 'dynamic_templates' => $typeConfig->getDynamicTemplates(), - // 'numeric_detection' => false, - // 'properties' => array(), - )); + $mapping = $typeConfig->getMapping(); + + if (null !== $typeConfig->getDynamicDateFormats()) { + $mapping['dynamic_date_formats'] = $typeConfig->getDynamicDateFormats(); + } + + if (null !== $typeConfig->getDateDetection()) { + $mapping['date_detection'] = $typeConfig->getDateDetection(); + } + + if (null !== $typeConfig->getNumericDetection()) { + $mapping['numeric_detection'] = $typeConfig->getNumericDetection(); + } if ($typeConfig->getIndexAnalyzer()) { $mapping['index_analyzer'] = $typeConfig->getIndexAnalyzer(); @@ -104,9 +110,14 @@ class MappingBuilder private function fixProperties(&$properties) { foreach ($properties as $name => &$property) { + unset($property['property_path']); + if (!isset($property['type'])) { $property['type'] = 'string'; } + if ($property['type'] == 'multi_field' && isset($property['fields'])) { + $this->fixProperties($property['fields']); + } if (isset($property['properties'])) { $this->fixProperties($property['properties']); } diff --git a/Index/Resetter.php b/Index/Resetter.php index 996bfdf..d2b3cea 100644 --- a/Index/Resetter.php +++ b/Index/Resetter.php @@ -62,6 +62,9 @@ class Resetter /** * Deletes and recreates all indexes + * + * @param bool $populating + * @param bool $force */ public function resetAllIndexes($populating = false, $force = false) { diff --git a/Propel/Provider.php b/Propel/Provider.php index 38f7a61..a3af1bd 100644 --- a/Propel/Provider.php +++ b/Propel/Provider.php @@ -12,7 +12,7 @@ use FOS\ElasticaBundle\Provider\AbstractProvider; class Provider extends AbstractProvider { /** - * @see FOS\ElasticaBundle\Provider\ProviderInterface::populate() + * {@inheritDoc} */ public function populate(\Closure $loggerClosure = null, array $options = array()) { @@ -23,34 +23,21 @@ class Provider extends AbstractProvider $batchSize = isset($options['batch-size']) ? intval($options['batch-size']) : $this->options['batch_size']; for (; $offset < $nbObjects; $offset += $batchSize) { - if ($loggerClosure) { - $stepStartTime = microtime(true); - } - $objects = $queryClass::create() ->limit($batchSize) ->offset($offset) ->find() ->getArrayCopy(); - if ($loggerClosure) { - $stepNbObjects = count($objects); - } + $objects = array_filter($objects, array($this, 'isObjectIndexable')); - if (!$objects) { - $loggerClosure('Entire batch was filtered away, skipping...'); - - continue; + if ($objects) { + $this->objectPersister->insertMany($objects); } - $this->objectPersister->insertMany($objects); - usleep($sleep); if ($loggerClosure) { - $stepCount = $stepNbObjects + $offset; - $percentComplete = 100 * $stepCount / $nbObjects; - $objectsPerSecond = $stepNbObjects / (microtime(true) - $stepStartTime); - $loggerClosure(sprintf('%0.1f%% (%d/%d), %d objects/s %s', $percentComplete, $stepCount, $nbObjects, $objectsPerSecond, $this->getMemoryUsage())); + $loggerClosure($batchSize, $nbObjects); } } } diff --git a/Provider/AbstractProvider.php b/Provider/AbstractProvider.php index 82ea914..842518d 100644 --- a/Provider/AbstractProvider.php +++ b/Provider/AbstractProvider.php @@ -70,6 +70,7 @@ abstract class AbstractProvider implements ProviderInterface /** * Get string with RAM usage information (current and peak) * + * @deprecated To be removed in 4.0 * @return string */ protected function getMemoryUsage() diff --git a/Provider/ProviderRegistry.php b/Provider/ProviderRegistry.php index 2142223..389d972 100644 --- a/Provider/ProviderRegistry.php +++ b/Provider/ProviderRegistry.php @@ -58,7 +58,7 @@ class ProviderRegistry implements ContainerAwareInterface * Providers will be indexed by "type" strings in the returned array. * * @param string $index - * @return array of ProviderInterface instances + * @return ProviderInterface[] * @throws \InvalidArgumentException if no providers were registered for the index */ public function getIndexProviders($index) diff --git a/Resources/config/mongodb.xml b/Resources/config/mongodb.xml index 8e15533..703c1d2 100644 --- a/Resources/config/mongodb.xml +++ b/Resources/config/mongodb.xml @@ -23,7 +23,6 @@ - diff --git a/Resources/config/transformer.xml b/Resources/config/transformer.xml index 4ce5062..0957152 100644 --- a/Resources/config/transformer.xml +++ b/Resources/config/transformer.xml @@ -12,7 +12,8 @@ - + + diff --git a/Resources/doc/cookbook/custom-properties.md b/Resources/doc/cookbook/custom-properties.md new file mode 100644 index 0000000..1d7687e --- /dev/null +++ b/Resources/doc/cookbook/custom-properties.md @@ -0,0 +1,33 @@ +##### Custom Properties + +Since FOSElasticaBundle 3.1.0, we now dispatch an event for each transformation of an +object into an Elastica document which allows you to set custom properties on the Elastica +document for indexing. + +Set up an event listener or subscriber for +`FOS\ElasticaBundle\Event\TransformEvent::POST_TRANSFORM` to be able to inject your own +parameters. + +```php +class CustomPropertyListener implements EventSubscriberInterface +{ + private $anotherService; + + // ... + + public function addCustomProperty(TransformEvent $event) + { + $document = $event->getDocument(); + $custom = $this->anotherService->calculateCustom($event->getObject()); + + $document->set('custom', $custom); + } + + public static function getSubscribedEvents() + { + return array( + TransformEvent::POST_TRANSFORM => 'addCustomProperty', + ); + } +} +``` diff --git a/Resources/doc/cookbook/manual-provider.md b/Resources/doc/cookbook/manual-provider.md index f4365da..ed5568e 100644 --- a/Resources/doc/cookbook/manual-provider.md +++ b/Resources/doc/cookbook/manual-provider.md @@ -8,7 +8,7 @@ index and type for which the service will provide. # app/config/config.yml services: acme.search_provider.user: - class: Acme\UserBundle\Search\UserProvider + class: Acme\UserBundle\Provider\UserProvider arguments: - @fos_elastica.index.website.user tags: diff --git a/Resources/doc/cookbook/multiple-connections.md b/Resources/doc/cookbook/multiple-connections.md index 7b5226c..9544359 100644 --- a/Resources/doc/cookbook/multiple-connections.md +++ b/Resources/doc/cookbook/multiple-connections.md @@ -11,6 +11,11 @@ fos_elastica: connections: - url: http://es1.example.net:9200 - url: http://es2.example.net:9200 + connection_strategy: RoundRobin ``` +Elastica allows for definition of different connection strategies and by default +supports `RoundRobin` and `Simple`. You can see definitions for these strategies +in the `Elastica\Connection\Strategy` namespace. + For more information on Elastica clustering see http://elastica.io/getting-started/installation.html#section-connect-cluster diff --git a/Resources/doc/cookbook/suppress-server-errors.md b/Resources/doc/cookbook/suppress-server-errors.md index e4e371e..72c7b38 100644 --- a/Resources/doc/cookbook/suppress-server-errors.md +++ b/Resources/doc/cookbook/suppress-server-errors.md @@ -23,7 +23,7 @@ namespace Acme\ElasticaBundle; use Elastica\Exception\ExceptionInterface; use Elastica\Request; use Elastica\Response; -use FOS\ElasticaBundle\Client as BaseClient; +use FOS\ElasticaBundle\Elastica\Client as BaseClient; class Client extends BaseClient { diff --git a/Resources/doc/index.md b/Resources/doc/index.md index 349723b..c856798 100644 --- a/Resources/doc/index.md +++ b/Resources/doc/index.md @@ -13,6 +13,7 @@ Cookbook Entries ---------------- * [Aliased Indexes](cookbook/aliased-indexes.md) +* [Custom Indexed Properties](cookbook/custom-properties.md) * [Custom Repositories](cookbook/custom-repositories.md) * [HTTP Headers for Elastica](cookbook/elastica-client-http-headers.md) * Performance - [Logging](cookbook/logging.md) diff --git a/Resources/doc/setup.md b/Resources/doc/setup.md index 16e427f..ea3f769 100644 --- a/Resources/doc/setup.md +++ b/Resources/doc/setup.md @@ -1,40 +1,50 @@ Step 1: Setting up the bundle ============================= -A) Install FOSElasticaBundle ----------------------------- +A: Download the Bundle +---------------------- -FOSElasticaBundle is installed using [Composer](https://getcomposer.org). +Open a command console, enter your project directory and execute the +following command to download the latest stable version of this bundle: ```bash -$ php composer.phar require friendsofsymfony/elastica-bundle +$ composer require friendsofsymfony/elastica-bundle "~3.0" ``` +This command requires you to have Composer installed globally, as explained +in the [installation chapter](https://getcomposer.org/doc/00-intro.md) +of the Composer documentation. + ### Elasticsearch -Instructions for installing and deploying Elasticsearch may be found -[here](http://www.elasticsearch.org/guide/reference/setup/installation/). +Instructions for installing and deploying Elasticsearch may be found [here](http://www.elasticsearch.org/guide/reference/setup/installation/). +Step 2: Enable the Bundle +------------------------- -B) Enable FOSElasticaBundle ---------------------------- - -Enable FOSElasticaBundle in your AppKernel: +Then, enable the bundle by adding the following line in the `app/AppKernel.php` +file of your project: ```php 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 ----------------------------------------------- diff --git a/Resources/doc/usage.md b/Resources/doc/usage.md index 37514b3..588532c 100644 --- a/Resources/doc/usage.md +++ b/Resources/doc/usage.md @@ -160,7 +160,7 @@ fos_elastica: site: settings: index: - analysis: + analysis: analyzer: my_analyzer: type: snowball diff --git a/Resources/public/images/elastica.png b/Resources/public/images/elastica.png deleted file mode 100644 index dbde014..0000000 Binary files a/Resources/public/images/elastica.png and /dev/null differ diff --git a/Resources/views/Collector/elastica.html.twig b/Resources/views/Collector/elastica.html.twig index e6d7072..82a3dcf 100644 --- a/Resources/views/Collector/elastica.html.twig +++ b/Resources/views/Collector/elastica.html.twig @@ -23,7 +23,7 @@ {% block menu %} - + Elastica {{ collector.querycount }} diff --git a/Tests/DependencyInjection/FOSElasticaExtensionTest.php b/Tests/DependencyInjection/FOSElasticaExtensionTest.php new file mode 100644 index 0000000..feb520f --- /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/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/MappingToElasticaTest.php b/Tests/Functional/MappingToElasticaTest.php index f42df61..197dcca 100644 --- a/Tests/Functional/MappingToElasticaTest.php +++ b/Tests/Functional/MappingToElasticaTest.php @@ -32,6 +32,10 @@ class MappingToElasticaTest extends WebTestCase $this->assertTrue($mapping['type']['properties']['field1']['store']); $this->assertArrayNotHasKey('store', $mapping['type']['properties']['field2']); + $type = $this->getType($client, 'type'); + $mapping = $type->getMapping(); + $this->assertEquals('parent', $mapping['type']['_parent']['type']); + $parent = $this->getType($client, 'parent'); $mapping = $parent->getMapping(); @@ -49,6 +53,9 @@ class MappingToElasticaTest extends WebTestCase $mapping = $type->getMapping(); $this->assertNotEmpty($mapping, 'Mapping was populated'); + $this->assertFalse($mapping['type']['date_detection']); + $this->assertTrue($mapping['type']['numeric_detection']); + $this->assertEquals(array('yyyy-MM-dd'), $mapping['type']['dynamic_date_formats']); $this->assertArrayHasKey('store', $mapping['type']['properties']['field1']); $this->assertTrue($mapping['type']['properties']['field1']['store']); $this->assertArrayNotHasKey('store', $mapping['type']['properties']['field2']); @@ -105,6 +112,7 @@ class MappingToElasticaTest extends WebTestCase /** * @param Client $client + * @param string $type * @return \Elastica\Type */ private function getType(Client $client, $type = 'type') diff --git a/Tests/Functional/PropertyPathTest.php b/Tests/Functional/PropertyPathTest.php new file mode 100644 index 0000000..860cb86 --- /dev/null +++ b/Tests/Functional/PropertyPathTest.php @@ -0,0 +1,54 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace FOS\ElasticaBundle\Tests\Functional; + +use Elastica\Query\Match; + +/** + * @group functional + */ +class PropertyPathTest extends WebTestCase +{ + public function testContainerSource() + { + $client = $this->createClient(array('test_case' => 'ORM')); + /** @var \FOS\ElasticaBundle\Persister\ObjectPersister $persister */ + $persister = $client->getContainer()->get('fos_elastica.object_persister.index.property_paths_type'); + $obj = new TypeObj(); + $obj->coll = 'Hello'; + $persister->insertOne($obj); + + /** @var \Elastica\Index $elClient */ + $index = $client->getContainer()->get('fos_elastica.index.index'); + $index->flush(true); + + $query = new Match(); + $query->setField('something', 'Hello'); + $search = $index->createSearch($query); + + $this->assertEquals(1, $search->count()); + } + + protected function setUp() + { + parent::setUp(); + + $this->deleteTmpDir('Basic'); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->deleteTmpDir('Basic'); + } +} diff --git a/Tests/Functional/TypeObj.php b/Tests/Functional/TypeObj.php index 46e5968..39e9fe9 100644 --- a/Tests/Functional/TypeObj.php +++ b/Tests/Functional/TypeObj.php @@ -13,8 +13,10 @@ namespace FOS\ElasticaBundle\Tests\Functional; class TypeObj { + public $id = 5; public $coll; public $field1; + public $field2; public function isIndexable() { diff --git a/Tests/Functional/app/Basic/config.yml b/Tests/Functional/app/Basic/config.yml index 3c3d369..8ed88eb 100644 --- a/Tests/Functional/app/Basic/config.yml +++ b/Tests/Functional/app/Basic/config.yml @@ -15,7 +15,12 @@ fos_elastica: - url: http://localhost:9200 - host: localhost port: 9200 + connectionStrategy: RoundRobin second_server: + connections: + - url: http://localhost:9200 + connection_strategy: RoundRobin + third: url: http://localhost:9200 indexes: index: @@ -46,6 +51,8 @@ fos_elastica: index_analyzer: my_analyzer type: search_analyzer: my_analyzer + date_detection: false + dynamic_date_formats: [ 'yyyy-MM-dd' ] dynamic_templates: - dates: match: "date_*" @@ -56,6 +63,7 @@ fos_elastica: mapping: analyzer: english type: string + numeric_detection: true properties: field1: ~ field2: @@ -71,6 +79,11 @@ fos_elastica: properties: date: { boost: 5 } content: ~ + multiple: + type: "multi_field" + properties: + name: ~ + position: ~ user: type: "object" approver: @@ -87,3 +100,4 @@ fos_elastica: identifier: "id" null_mappings: mappings: ~ + empty_index: ~ diff --git a/Tests/Functional/app/ORM/config.yml b/Tests/Functional/app/ORM/config.yml index 98c9221..d2ff931 100644 --- a/Tests/Functional/app/ORM/config.yml +++ b/Tests/Functional/app/ORM/config.yml @@ -65,6 +65,18 @@ fos_elastica: provider: ~ listener: is_indexable_callback: [ 'FOS\ElasticaBundle\Tests\Functional\app\ORM\IndexableService', 'isntIndexable' ] + property_paths_type: + persistence: + driver: orm + model: FOS\ElasticaBundle\Tests\Functional\TypeObj + provider: ~ + properties: + field1: + property_path: field2 + something: + property_path: coll + dynamic: + property_path: false second_index: index_name: foselastica_orm_test_second_%kernel.environment% types: diff --git a/Tests/Functional/app/Serializer/config.yml b/Tests/Functional/app/Serializer/config.yml index 9bea4ba..de7caec 100644 --- a/Tests/Functional/app/Serializer/config.yml +++ b/Tests/Functional/app/Serializer/config.yml @@ -28,7 +28,7 @@ fos_elastica: serializer: ~ indexes: index: - index_name: foselastica_test_%kernel.environment% + index_name: foselastica_ser_test_%kernel.environment% types: type: properties: diff --git a/Tests/Transformer/ModelToElasticaAutoTransformerTest.php b/Tests/Transformer/ModelToElasticaAutoTransformerTest.php index 1fa6a8e..1dbf5fd 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; @@ -132,6 +133,35 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase } } + public function testTransformerDispatches() + { + $dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface') + ->getMock(); + $dispatcher->expects($this->once()) + ->method('dispatch') + ->with( + TransformEvent::POST_TRANSFORM, + $this->isInstanceOf('FOS\ElasticaBundle\Event\TransformEvent') + ); + + $transformer = $this->getTransformer($dispatcher); + $transformer->transform(new POPO(), array()); + } + + public function testPropertyPath() + { + $transformer = $this->getTransformer(); + + $document = $transformer->transform(new POPO(), array('name' => array('property_path' => false))); + $this->assertInstanceOf('Elastica\Document', $document); + $this->assertFalse($document->has('name')); + + $document = $transformer->transform(new POPO(), array('realName' => array('property_path' => 'name'))); + $this->assertInstanceOf('Elastica\Document', $document); + $this->assertTrue($document->has('realName')); + $this->assertEquals('someName', $document->get('realName')); + } + public function testThatCanTransformObject() { $transformer = $this->getTransformer(); @@ -250,7 +280,7 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $document = $transformer->transform(new POPO(), array( 'sub' => array( 'type' => 'nested', - 'properties' => array('foo' => '~') + 'properties' => array('foo' => array()) ) )); $data = $document->getData(); @@ -295,8 +325,8 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase $this->assertTrue(array_key_exists('obj', $data)); $this->assertInternalType('array', $data['obj']); $this->assertEquals(array( - 'foo' => 'foo', - 'bar' => 'foo', + 'foo' => 'foo', + 'bar' => 'foo', 'id' => 1 ), $data['obj']); } @@ -387,11 +417,12 @@ class ModelToElasticaAutoTransformerTest extends \PHPUnit_Framework_TestCase } /** + * @param null|\Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher * @return ModelToElasticaAutoTransformer */ - private function getTransformer() + private function getTransformer($dispatcher = null) { - $transformer = new ModelToElasticaAutoTransformer(); + $transformer = new ModelToElasticaAutoTransformer(array(), $dispatcher); $transformer->setPropertyAccessor(PropertyAccess::getPropertyAccessor()); return $transformer; diff --git a/Transformer/ModelToElasticaAutoTransformer.php b/Transformer/ModelToElasticaAutoTransformer.php index 106db15..6a9fbca 100644 --- a/Transformer/ModelToElasticaAutoTransformer.php +++ b/Transformer/ModelToElasticaAutoTransformer.php @@ -2,6 +2,8 @@ namespace FOS\ElasticaBundle\Transformer; +use FOS\ElasticaBundle\Event\TransformEvent; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Elastica\Document; @@ -12,6 +14,11 @@ use Elastica\Document; */ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterface { + /** + * @var EventDispatcherInterface + */ + protected $dispatcher; + /** * Optional parameters * @@ -32,10 +39,12 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf * Instanciates a new Mapper * * @param array $options + * @param EventDispatcherInterface $dispatcher */ - public function __construct(array $options = array()) + public function __construct(array $options = array(), EventDispatcherInterface $dispatcher = null) { $this->options = array_merge($this->options, $options); + $this->dispatcher = $dispatcher; } /** @@ -66,16 +75,24 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf $property = (null !== $mapping['property'])?$mapping['property']:$mapping['type']; $value = $this->propertyAccessor->getValue($object, $property); $document->setParent($this->propertyAccessor->getValue($value, $mapping['identifier'])); + continue; } - $value = $this->propertyAccessor->getValue($object, $key); + $path = isset($mapping['property_path']) ? + $mapping['property_path'] : + $key; + if (false === $path) { + continue; + } + $value = $this->propertyAccessor->getValue($object, $path); if (isset($mapping['type']) && in_array($mapping['type'], array('nested', 'object')) && isset($mapping['properties']) && !empty($mapping['properties'])) { /* $value is a nested document or object. Transform $value into * an array of documents, respective the mapped properties. */ $document->set($key, $this->transformNested($value, $mapping['properties'])); + continue; } @@ -86,12 +103,20 @@ class ModelToElasticaAutoTransformer implements ModelToElasticaTransformerInterf } else { $document->addFileContent($key, $value); } + continue; } $document->set($key, $this->normalizeValue($value)); } + if ($this->dispatcher) { + $event = new TransformEvent($document, $fields, $object); + $this->dispatcher->dispatch(TransformEvent::POST_TRANSFORM, $event); + + $document = $event->getDocument(); + } + return $document; } diff --git a/composer.json b/composer.json index 833621a..917e839 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "symfony/console": "~2.1", "symfony/form": "~2.1", "symfony/property-access": "~2.2", - "ruflin/elastica": ">=0.90.10.0, <1.4-dev", + "ruflin/elastica": ">=0.90.10.0, <1.5-dev", "psr/log": "~1.0" }, "require-dev":{